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"Data.TaggedError vs Schema.TaggedErrorClass
Section titled “Data.TaggedError vs Schema.TaggedErrorClass”Both define tagged, yieldable errors. Choose based on whether the error needs to cross a process boundary:
Data.TaggedError— lightweight, value-equality errors for in-process logic. No schema, no (de)serialization. Best when the error never leaves the current runtime.Schema.TaggedErrorClass(from Schema) — adds an encoded representation so the error can be serialized and reconstructed across RPC or HTTP API boundaries. Prefer it whenever an error is part of a wire contract.
Migrating from v3
Section titled “Migrating from v3”Effect v4’s Data module is smaller than v3’s. The Data.struct, Data.tuple,
Data.array, and Data.case helpers have been removed. Replace them as
follows:
- For value-equality records, define a
Data.Class(orData.TaggedClass) instead ofData.struct. - For ad-hoc collections that need structural equality, construct plain
arrays/objects and compare them with
Equal.equals, which already performs structural comparison. Data.case/Data.taggedconstructors are subsumed byData.taggedEnum, which generates per-variant constructors for you.
Generic tagged enums
Section titled “Generic tagged enums”When a variant’s payload is parameterized (e.g. a Result<E, A>), a plain type
alias cannot carry the generics through taggedEnum. Instead, extend
Data.TaggedEnum.WithGenerics<N> with an interface and pass that interface
to taggedEnum. The interface uses this["A"], this["B"], … as placeholders
for up to four generics.
import { Data } from "effect"
type MyResult<E, A> = Data.TaggedEnum<{ Failure: { readonly error: E } Success: { readonly value: A }}>
// Pass the *interface* (not the alias) to keep the genericsinterface MyResultDef extends Data.TaggedEnum.WithGenerics<2> { readonly taggedEnum: MyResult<this["A"], this["B"]>}
const { Failure, Success, $match } = Data.taggedEnum<MyResultDef>()
const ok = Success({ value: 42 })// => ok: { readonly _tag: "Success"; readonly value: number }
const describe = $match(ok, { Failure: (f) => `error: ${f.error}`, Success: (s) => `value: ${s.value}`})console.log(describe) // => "value: 42"Data.TaggedEnum.WithGenerics
Section titled “Data.TaggedEnum.WithGenerics”The interface you extend to declare a generic tagged enum. Set taggedEnum to
your union (using this["A"]…this["D"] for the type parameters); the numeric
type argument is the generic count (1–4). See the example above.
Data.TaggedEnum.Kind
Section titled “Data.TaggedEnum.Kind”Resolves a WithGenerics definition to its concrete tagged union for given type
arguments. Useful when referring to a specific instantiation in a signature.
import type { Data } from "effect"
type Option<A> = Data.TaggedEnum<{ None: {} Some: { readonly value: A }}>interface OptionDef extends Data.TaggedEnum.WithGenerics<1> { readonly taggedEnum: Option<this["A"]>}
type StringOption = Data.TaggedEnum.Kind<OptionDef, string>// => { readonly _tag: "None" } | { readonly _tag: "Some"; readonly value: string }Data.TaggedEnum.Args
Section titled “Data.TaggedEnum.Args”Extracts the constructor argument type for one variant — the variant’s fields
without _tag. Resolves to void when the variant has no extra fields.
import type { Data } from "effect"
type Result = | { readonly _tag: "Ok"; readonly value: number } | { readonly _tag: "Err"; readonly error: string }
type OkArgs = Data.TaggedEnum.Args<Result, "Ok">// => { readonly value: number }
type ErrArgs = Data.TaggedEnum.Args<Result, "Err">// => { readonly error: string }Data.TaggedEnum.Value
Section titled “Data.TaggedEnum.Value”Extracts the full variant type (including _tag) for a given tag.
import type { Data } from "effect"
type Result = | { readonly _tag: "Ok"; readonly value: number } | { readonly _tag: "Err"; readonly error: string }
type OkVariant = Data.TaggedEnum.Value<Result, "Ok">// => { readonly _tag: "Ok"; readonly value: number }Data.TaggedEnum.Constructor
Section titled “Data.TaggedEnum.Constructor”The type of the whole object returned by taggedEnum for a non-generic enum: one
ConstructorFrom per variant plus the $is guard and $match matcher. Use it
to annotate a value holding the constructors.
import type { Data } from "effect"
type Shape = | { readonly _tag: "Circle"; readonly radius: number } | { readonly _tag: "Rect"; readonly w: number; readonly h: number }
type ShapeCtors = Data.TaggedEnum.Constructor<Shape>// => { Circle: (args) => ...; Rect: (args) => ...; $is: ...; $match: ... }Data.TaggedEnum.ConstructorFrom
Section titled “Data.TaggedEnum.ConstructorFrom”The function type of a single variant constructor: it takes the variant’s fields
(excluding the keys named in Tag) and returns the full variant. The argument
type becomes void when no fields remain.
import type { Data } from "effect"
type Ok = { readonly _tag: "Ok"; readonly value: number }
type MakeOk = Data.TaggedEnum.ConstructorFrom<Ok, "_tag">// => (args: { readonly value: number }) => OkData.TaggedEnum.GenericMatchers
Section titled “Data.TaggedEnum.GenericMatchers”The $is / $match portion of the object returned by taggedEnum when used
with a WithGenerics definition. You rarely reference it directly — it is the
generic-aware counterpart to the matchers inside Constructor.
import type { Data } from "effect"
interface OptionDef extends Data.TaggedEnum.WithGenerics<1> { readonly taggedEnum: Data.TaggedEnum<{ None: {} Some: { readonly value: this["A"] } }>}
type Matchers = Data.TaggedEnum.GenericMatchers<OptionDef>// => { readonly $is: ...; readonly $match: ... }API reference
Section titled “API reference”The entire v4 Data surface: the value-class constructors (Class,
TaggedClass, Error, TaggedError), the TaggedEnum type plus its
taggedEnum constructor factory, and the TaggedEnum namespace of utility
types.
Data.Class
Section titled “Data.Class”Base class for plain immutable data. Extend it with a type parameter that
declares the fields; the constructor takes those fields as a single object (the
argument is optional when there are no fields). Instances are Readonly,
Pipeable, and compare by value.
import { Data, Equal } from "effect"
class Person extends Data.Class<{ readonly name: string }> {}
const mike1 = new Person({ name: "Mike" })const mike2 = new Person({ name: "Mike" })
console.log(Equal.equals(mike1, mike2)) // => true
// No-fields case: the constructor argument is optionalclass Anonymous extends Data.Class {}const anon = new Anonymous()console.log(anon instanceof Anonymous) // => trueData.TaggedClass
Section titled “Data.TaggedClass”Like Data.Class, but the instances also carry a readonly _tag literal you
supply at definition time. The _tag is set automatically and is excluded
from the constructor argument. Use it for a single tagged variant or an ad-hoc
discriminator; for multi-variant unions reach for taggedEnum.
import { Data } from "effect"
class Person extends Data.TaggedClass("Person")<{ readonly name: string}> {}
const mike = new Person({ name: "Mike" })console.log(mike._tag) // => "Person"console.log(mike.name) // => "Mike"
// No-fields tagged class: constructor argument is optionalclass Ping extends Data.TaggedClass("Ping") {}console.log(new Ping()._tag) // => "Ping"Data.TaggedEnum (type)
Section titled “Data.TaggedEnum (type)”A type-level helper that turns a record of variant definitions into a
discriminated union, adding readonly _tag to each variant from its record key.
Variant records must not already contain a _tag key. Pair it with
taggedEnum to obtain runtime constructors and matchers.
import type { Data } from "effect"
type HttpError = Data.TaggedEnum<{ BadRequest: { readonly status: 400; readonly message: string } NotFound: { readonly status: 404 }}>// => | { readonly _tag: "BadRequest"; readonly status: 400; readonly message: string }// => | { readonly _tag: "NotFound"; readonly status: 404 }Data.taggedEnum
Section titled “Data.taggedEnum”Creates the runtime constructors and matchers for a TaggedEnum. The returned
object has one constructor per variant (keyed by tag), plus $is and $match.
Constructors produce plain objects (not class instances) with _tag filled
in. For no-field variants the constructor argument is omitted.
import { Data } from "effect"
type HttpError = Data.TaggedEnum<{ BadRequest: { readonly message: string } NotFound: { readonly url: string }}>
const { BadRequest, NotFound, $is, $match } = Data.taggedEnum<HttpError>()
const err = NotFound({ url: "/missing" })console.log(err) // => { url: "/missing", _tag: "NotFound" }$is(tag) — type guard
Section titled “$is(tag) — type guard”$is(tag) returns a type guard that narrows a value to a single variant. It
checks only the _tag field, so it is safe for trusted values produced by
your constructors; validate untrusted input with Schema first.
import { Data } from "effect"
type HttpError = Data.TaggedEnum<{ BadRequest: { readonly message: string } NotFound: { readonly url: string }}>const { NotFound, $is } = Data.taggedEnum<HttpError>()
const err = NotFound({ url: "/missing" })console.log($is("NotFound")(err)) // => trueconsole.log($is("BadRequest")(err)) // => false$match — exhaustive matching
Section titled “$match — exhaustive matching”$match dispatches on _tag. It is overloaded: pass (value, cases) for
data-first, or (cases) for a data-last matcher you can reuse and .pipe()
into. Missing a case is a compile error.
import { Data } from "effect"
type HttpError = Data.TaggedEnum<{ BadRequest: { readonly message: string } NotFound: { readonly url: string }}>const { BadRequest, NotFound, $match } = Data.taggedEnum<HttpError>()
// data-firstconsole.log( $match(NotFound({ url: "/x" }), { BadRequest: (e) => e.message, NotFound: (e) => `${e.url} not found` })) // => "/x not found"
// data-last (reusable matcher)const toMessage = $match({ BadRequest: (e) => e.message, NotFound: (e) => `${e.url} not found`})console.log(toMessage(BadRequest({ message: "bad" }))) // => "bad"Data.Error
Section titled “Data.Error”Base class for yieldable errors without a tag. Extends
Cause.YieldableError, so an instance can be yield*-ed inside Effect.gen to
fail the surrounding effect. Fields are passed as one object; a message field
becomes the error’s .message.
import { Data, Effect } from "effect"
class NetworkError extends Data.Error<{ readonly code: number readonly message: string}> {}
const program = Effect.gen(function* () { return yield* new NetworkError({ code: 500, message: "timeout" })})
Effect.runPromise(Effect.flip(program)).then((e) => console.log(e.code, e.message)) // => 500 "timeout"Data.TaggedError
Section titled “Data.TaggedError”Like Data.Error, but instances also carry a readonly _tag, enabling
Effect.catchTag / Effect.catchTags for tag-based recovery. The _tag is
excluded from the constructor argument. This is the recommended way to model
in-process domain errors.
import { Data, Effect } from "effect"
class NotFound extends Data.TaggedError("NotFound")<{ readonly resource: string}> {}
const program = Effect.gen(function* () { return yield* new NotFound({ resource: "/users/42" })})
const recovered = program.pipe( Effect.catchTag("NotFound", (e) => Effect.succeed(`missing: ${e.resource}`)))
Effect.runPromise(recovered).then(console.log) // => "missing: /users/42"See also
Section titled “See also”- Equal & Hash — the value-equality trait that all
Datainstances implement. - Pattern Matching — the standalone
Matchmodule, for matching beyond$match. - Tagged errors — recovering from
Data.TaggedErrorwithcatchTag/catchTags. - Defining errors with Schema — serializable errors for RPC/HTTP boundaries.