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 objectclass 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 equalityconsole.log(a === b) // falseconsole.log(Equal.equals(a, b)) // trueData.Class builds an immutable value type. Instances are Readonly, support
.pipe(), and — crucially — are equal when their fields are equal.
Tagged structs
Section titled “Tagged structs”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.
Tagged unions
Section titled “Tagged unions”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 statestype RemoteData = Data.TaggedEnum<{ Loading: {} Success: { readonly data: string } Failure: { readonly error: string }}>
// Generate constructors + helpers for the unionconst { 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" }))) // trueTagged errors
Section titled “Tagged errors”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.