Skip to content

Data

Plain JavaScript objects compare by reference: two objects with identical contents are not ===, and they hash differently in a Set. The Data module fixes this. It provides base classes whose instances compare by value via Equal.equals, which makes them safe to compare in tests and to use as keys in HashMap/HashSet. Data also gives you ergonomic discriminated unions through TaggedEnum.

import { Data, Equal } from "effect"
// A value class: fields are declared via the type parameter,
// and passed to the constructor as a single object
class Point extends Data.Class<{
readonly x: number
readonly y: number
}> {}
const a = new Point({ x: 1, y: 2 })
const b = new Point({ x: 1, y: 2 })
// Reference equality would say false; Data gives structural equality
console.log(a === b) // false
console.log(Equal.equals(a, b)) // true

Data.Class builds an immutable value type. Instances are Readonly, support .pipe(), and — crucially — are equal when their fields are equal.

Data.TaggedClass adds a readonly _tag discriminator, which you can match on. This is the building block for modelling one variant of a domain type.

import { Data } from "effect"
class User extends Data.TaggedClass("User")<{
readonly id: number
readonly name: string
}> {}
const user = new User({ id: 1, name: "Mike" })
console.log(user._tag) // "User"
console.log(user.name) // "Mike"

The _tag is set for you — you do not pass it to the constructor.

For a type with several variants, declare a Data.TaggedEnum and generate its constructors and helpers with Data.taggedEnum. Each variant becomes a constructor, and you also get $is (a type guard) and $match (exhaustive pattern matching).

import { Data } from "effect"
// A discriminated union of remote-data states
type RemoteData = Data.TaggedEnum<{
Loading: {}
Success: { readonly data: string }
Failure: { readonly error: string }
}>
// Generate constructors + helpers for the union
const { Loading, Success, Failure, $match } = Data.taggedEnum<RemoteData>()
const render = $match({
Loading: () => "loading…",
Success: ({ data }) => `loaded: ${data}`,
Failure: ({ error }) => `error: ${error}`
})
console.log(render(Loading())) // "loading…"
console.log(render(Success({ data: "hello" }))) // "loaded: hello"
console.log(render(Failure({ error: "timeout" }))) // "error: timeout"

$match is exhaustive: omit a case and the code will not compile, so adding a new variant forces you to handle it everywhere. Use $is("Success") when you just need a type guard for one variant. Because the variants are Data values, they also compare structurally:

import { Data, Equal } from "effect"
type RemoteData = Data.TaggedEnum<{
Success: { readonly data: string }
}>
const { Success } = Data.taggedEnum<RemoteData>()
console.log(Equal.equals(Success({ data: "x" }), Success({ data: "x" }))) // true

Data.TaggedError is the same idea applied to errors: it produces a tagged class that is also yieldable in Effect.gen, so you can yield* an instance to fail an effect. It pairs naturally with Error Management, where Effect.catchTag dispatches on the _tag.

import { Data, Effect } from "effect"
class NotFound extends Data.TaggedError("NotFound")<{
readonly id: number
}> {}
const find = Effect.fn("find")(function* (id: number) {
if (id < 0) {
// Yielding the error fails the effect with NotFound
return yield* new NotFound({ id })
}
return `record ${id}`
})
const program = find(-1).pipe(
// catchTag narrows on `_tag` and gives you the typed fields
Effect.catchTag("NotFound", (e) => Effect.succeed(`missing #${e.id}`))
)
Effect.runPromise(program).then(console.log) // "missing #-1"

For schema-driven errors that also serialize across process boundaries (e.g. RPC or HTTP API), prefer Schema.TaggedErrorClass from the Schema module. Use Data.TaggedError when you want a lightweight error with value equality and no serialization concerns.