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"

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.

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 (or Data.TaggedClass) instead of Data.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.tagged constructors are subsumed by Data.taggedEnum, which generates per-variant constructors for you.

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 generics
interface 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"

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.

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 }

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 }

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 }

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: ... }

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 }) => Ok

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: ... }

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.

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 optional
class Anonymous extends Data.Class {}
const anon = new Anonymous()
console.log(anon instanceof Anonymous) // => true

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 optional
class Ping extends Data.TaggedClass("Ping") {}
console.log(new Ping()._tag) // => "Ping"

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 }

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) 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)) // => true
console.log($is("BadRequest")(err)) // => false

$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-first
console.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"

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"

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"