Classes & Opaque Types
Schema.Class turns a struct schema into a class. You get validated
construction, a named type you can attach methods to, and — because the class
extends Effect’s Data — value-based equality out of the box. It is the
idiomatic way to model a domain entity that is more than a bag of fields.
import { Schema } from "effect"
// `class Self extends Schema.Class<Self>("Identifier")({ fields }) {}`class Person extends Schema.Class<Person>("Person")({ firstName: Schema.String.check(Schema.isNonEmpty()), lastName: Schema.String.check(Schema.isNonEmpty()), age: Schema.Number.check(Schema.isGreaterThanOrEqualTo(0))}) { // Regular methods and getters work as you would expect. get fullName(): string { return `${this.firstName} ${this.lastName}` }}
// The constructor validates its input against the schema.const alice = new Person({ firstName: "Alice", lastName: "Ng", age: 30 })
console.log(alice.fullName) // => "Alice Ng"console.log(`${alice}`) // => "Person({ firstName: Alice, lastName: Ng, age: 30 })"Why a class instead of a struct
Section titled “Why a class instead of a struct”A plain Schema.Struct produces anonymous objects. A Schema.Class gives you:
- Validated construction —
new Person({...})runs the schema’s checks and throws on invalid input, so an instance is always valid. - Identity — the type has a name (
Person), useful in errors, logs, and pattern matching. - Behaviour — methods and getters live alongside the data.
- Value equality — two instances with equal fields are
Equal.equals.
The class is a schema, so you can decode and encode through it just like any other schema.
import { Schema } from "effect"
class Person extends Schema.Class<Person>("Person")({ name: Schema.String, age: Schema.Number}) {}
// Decode unknown input straight into a Person instance.const person = Schema.decodeUnknownSync(Person)({ name: "Bob", age: 25 })
console.log(person instanceof Person) // => trueValue-based equality
Section titled “Value-based equality”Because the class extends Data.Class, instances compare by value rather than
by reference. This makes class instances safe to use as keys in a HashMap,
members of a HashSet, or anywhere Effect relies on structural equality (see
Equal & Hash).
import { Equal, Schema } from "effect"
class Point extends Schema.Class<Point>("Point")({ x: Schema.Number, y: Schema.Number}) {}
const a = new Point({ x: 1, y: 2 })const b = new Point({ x: 1, y: 2 })
console.log(Equal.equals(a, b)) // => true — same field valuesconsole.log(a === b) // => false — different referencesEffectful construction
Section titled “Effectful construction”new Person({...}) throws on invalid input, which is fine inside trusted code
where the inputs are statically known. When you would rather keep failures in
the error channel, use the static makeEffect — it
returns an Effect that fails with a SchemaError.
import { Effect, Schema } from "effect"
class Person extends Schema.Class<Person>("Person")({ name: Schema.String.check(Schema.isNonEmpty()), age: Schema.Number.check(Schema.isGreaterThanOrEqualTo(0))}) {}
const program = Effect.gen(function*() { // Validation failures surface as a typed SchemaError, not a thrown exception. const person = yield* Person.makeEffect({ name: "Carol", age: 41 }) yield* Effect.log(`Created ${person.name}`) return person})Tagged classes
Section titled “Tagged classes”Schema.TaggedClass adds a _tag literal field automatically, so instances
participate in tagged-union pattern matching. Note the extra () — the Self
generic is supplied first, then the tag and fields are passed at the next call.
import { Schema } from "effect"
class Circle extends Schema.TaggedClass<Circle>()("Circle", { radius: Schema.Number}) {}
const c = new Circle({ radius: 5 })
console.log(c._tag) // => "Circle"console.log(c.radius) // => 5The _tag is auto-populated as a constructor default, so you never pass it.
Combine several tagged classes into a Schema.Union and switch on _tag:
import { Schema } from "effect"
class Circle extends Schema.TaggedClass<Circle>()("Circle", { radius: Schema.Number}) {}
class Square extends Schema.TaggedClass<Square>()("Square", { side: Schema.Number}) {}
const Shape = Schema.Union([Circle, Square])
function area(shape: typeof Shape.Type): number { switch (shape._tag) { case "Circle": return Math.PI * shape.radius ** 2 case "Square": return shape.side ** 2 }}
console.log(area(new Circle({ radius: 2 }))) // => 12.566...console.log(area(new Square({ side: 3 }))) // => 9Extending a class
Section titled “Extending a class”Every Schema.Class exposes a static extend to derive a subclass with extra
fields, inheriting the parent’s fields, checks, and methods.
import { Schema } from "effect"
class Animal extends Schema.Class<Animal>("Animal")({ name: Schema.String}) {}
class Dog extends Animal.extend<Dog>("Dog")({ breed: Schema.String}) {}
const dog = new Dog({ name: "Rex", breed: "Labrador" })
console.log(dog.name) // => "Rex"console.log(dog.breed) // => "Labrador"API reference
Section titled “API reference”Schema.Class
Section titled “Schema.Class”Schema.Class<Self>(identifier)(fieldsOrStruct, annotations?) produces a class
whose constructor validates input and whose instances carry value identity. You
pass either a plain fields object or an existing Schema.Struct.
import { Schema } from "effect"
// From a fields objectclass A extends Schema.Class<A>("A")({ n: Schema.Number }) {}
// From an existing Struct (reuse a struct schema you already have)const Fields = Schema.Struct({ n: Schema.Number })class B extends Schema.Class<B>("B")(Fields) {}
console.log(new A({ n: 1 }).n) // => 1console.log(new B({ n: 2 }).n) // => 2The class is a full schema — decode and encode through it directly:
import { Schema } from "effect"
class Person extends Schema.Class<Person>("Person")({ name: Schema.String, age: Schema.Number}) {}
const decode = Schema.decodeUnknownSync(Person)console.log(decode({ name: "Bob", age: 25 }) instanceof Person) // => true
const encode = Schema.encodeUnknownSync(Person)console.log(encode(new Person({ name: "Bob", age: 25 })))// => { name: "Bob", age: 25 }new (constructor)
Section titled “new (constructor)”new Self(props, options?) validates props against the schema and throws on
failure. Pass { disableChecks: true } in options to skip validation when you
trust the data.
import { Schema } from "effect"
class Age extends Schema.Class<Age>("Age")({ value: Schema.Number.check(Schema.isGreaterThanOrEqualTo(0))}) {}
console.log(new Age({ value: 30 }).value) // => 30
// Bypass validation for trusted data:console.log(new Age({ value: -1 }, { disableChecks: true }).value) // => -1Self.make
Section titled “Self.make”Static make(props, options?) is the schema-style constructor, equivalent to
new Self(...). It throws on invalid input and applies constructor defaults.
import { Schema } from "effect"
class Person extends Schema.Class<Person>("Person")({ name: Schema.String}) {}
console.log(Person.make({ name: "Alice" }).name) // => "Alice"Self.makeOption
Section titled “Self.makeOption”Static makeOption(props, options?) returns an Option<Self> — Some on
success, None on validation failure — instead of throwing.
import { Option, Schema } from "effect"
class Age extends Schema.Class<Age>("Age")({ value: Schema.Number.check(Schema.isGreaterThanOrEqualTo(0))}) {}
console.log(Option.isSome(Age.makeOption({ value: 5 }))) // => trueconsole.log(Age.makeOption({ value: -1 })) // => none()Self.makeEffect
Section titled “Self.makeEffect”Static makeEffect(props, options?) returns Effect<Self, SchemaError>,
moving validation failures into the error channel.
import { Effect, Schema } from "effect"
class Age extends Schema.Class<Age>("Age")({ value: Schema.Number.check(Schema.isGreaterThanOrEqualTo(0))}) {}
const program = Effect.gen(function*() { const age = yield* Age.makeEffect({ value: 5 }) return age.value // => 5})Self.fields
Section titled “Self.fields”Static fields exposes the field schemas, so you can reuse or recompose them
when building other schemas.
import { Schema } from "effect"
class Person extends Schema.Class<Person>("Person")({ name: Schema.String, age: Schema.Number}) {}
// Reuse a subset of the class's fields in another struct.const NameOnly = Schema.Struct({ name: Person.fields.name })
console.log(Object.keys(Person.fields)) // => ["name", "age"]Self.identifier
Section titled “Self.identifier”Static identifier returns the string identifier the class was created with.
import { Schema } from "effect"
class Person extends Schema.Class<Person>("Person")({ name: Schema.String }) {}
console.log(Person.identifier) // => "Person"Self.extend
Section titled “Self.extend”Static extend<Extended>(identifier)(newFields, annotations?) derives a
subclass that inherits the parent’s fields, checks, and methods, and adds the
new fields. New fields with the same key override the parent’s.
import { Schema } from "effect"
class Animal extends Schema.Class<Animal>("Animal")({ name: Schema.String}) { greet() { return `Hi, I'm ${this.name}` }}
class Dog extends Animal.extend<Dog>("Dog")({ breed: Schema.String}) {}
const dog = new Dog({ name: "Rex", breed: "Labrador" })console.log(dog.greet()) // => "Hi, I'm Rex" (inherited method)console.log(dog.breed) // => "Labrador"Self.mapFields
Section titled “Self.mapFields”Static mapFields(f, options?) returns a new Struct (not a class) whose
fields are derived from the class’s fields by f. Useful for composing a new
schema from an existing class’s shape.
import { Schema, Struct } from "effect"
class Person extends Schema.Class<Person>("Person")({ name: Schema.String, age: Schema.Number}) {}
// Make every field optional in a derived struct. `Schema.optional` is a Struct// lambda, so `Struct.map` applies it to each field.const PartialPerson = Person.mapFields((fields) => Struct.map(fields, Schema.optional))
console.log(Object.keys(PartialPerson.fields)) // => ["name", "age"]Self.annotate / annotateKey / check
Section titled “Self.annotate / annotateKey / check”The class inherits the standard schema combinators. annotate and annotateKey
attach annotations and return a rebuilt schema; check appends a refinement
that validates the constructed instance.
import { Schema } from "effect"
class Range extends Schema.Class<Range>("Range")({ min: Schema.Number, max: Schema.Number}) {}
// Add an instance-level invariant.const ValidRange = Range.check( Schema.makeFilter((r) => (r.min <= r.max ? undefined : "min must be <= max")))
console.log(Schema.is(ValidRange)(new Range({ min: 1, max: 2 }))) // => trueconsole.log(Schema.is(ValidRange)(new Range({ min: 5, max: 2 }))) // => falseConstructor defaults
Section titled “Constructor defaults”Fields built with Schema.withConstructorDefault supply a value when omitted
from make/new. Defaults apply only during construction, never during
decoding or encoding.
import { Effect, Schema } from "effect"
class Config extends Schema.Class<Config>("Config")({ retries: Schema.Number.pipe( Schema.optionalKey, Schema.withConstructorDefault(Effect.succeed(3)) )}) {}
console.log(new Config({}).retries) // => 3console.log(new Config({ retries: 5 }).retries) // => 5Schema.TaggedClass
Section titled “Schema.TaggedClass”Schema.TaggedClass<Self>()(tag, fields, annotations?) is Class plus an
auto-populated _tag literal. The optional first call supplies Self; the
tag string both names the schema (by default) and becomes the _tag value.
import { Schema } from "effect"
class Loaded extends Schema.TaggedClass<Loaded>()("Loaded", { data: Schema.String}) {}
const l = new Loaded({ data: "ok" })console.log(l._tag) // => "Loaded"
// Decode preserves the tag.console.log(Schema.decodeUnknownSync(Loaded)({ _tag: "Loaded", data: "ok" })._tag)// => "Loaded"Schema.Opaque
Section titled “Schema.Opaque”Schema.Opaque<Self>()(schema) wraps a schema (typically a Struct) so its
decoded Type becomes the nominal type Self, without changing the runtime
shape. Unlike Class, instances are plain objects — there is no constructor,
no methods, and no _tag; you just get a named, opaque type alias backed by the
struct.
import { Schema } from "effect"
class User extends Schema.Opaque<User>()( Schema.Struct({ id: Schema.Number, name: Schema.String })) {}
// Decoded value is `User`, but at runtime it is a plain object.const user = Schema.decodeUnknownSync(User)({ id: 1, name: "Alice" })console.log(user) // => { id: 1, name: "Alice" }console.log(user instanceof User) // => false (Opaque has no instances)Because the opaque value’s type is Self, you can refer to it by name and
read its fields directly off the schema (it inherits the struct’s fields,
make, etc.):
import { Schema } from "effect"
class User extends Schema.Opaque<User>()( Schema.Struct({ id: Schema.Number, name: Schema.String })) {}
// `make` comes from the underlying struct.const u = User.make({ id: 2, name: "Bob" })console.log(u.name) // => "Bob"
// Field schemas are accessible too.console.log(Object.keys(User.fields)) // => ["id", "name"]Schema.asClass
Section titled “Schema.asClass”Schema.asClass(schema) turns any schema (not just structs) into a class-like
constructor so you can extends it and attach static members that reference
this. The added construct signature is new(_: never): {} — it is not meant
to be instantiated; it exists so you can hang statics and a name on a schema.
import { Schema } from "effect"
class MyString extends Schema.asClass(Schema.String) { static readonly decodeUnknownSync = Schema.decodeUnknownSync(this)}
console.log(MyString.decodeUnknownSync("a")) // => "a"Schema.ErrorClass / Schema.TaggedErrorClass
Section titled “Schema.ErrorClass / Schema.TaggedErrorClass”Schema.ErrorClass and Schema.TaggedErrorClass build on Class
(TaggedErrorClass also adds a _tag) and additionally implement Effect’s
yieldable error interface, so instances can be yield*-ed and matched with
Effect.catchTag. These are documented in full on the
defining errors page.
import { Schema } from "effect"
class NotFound extends Schema.TaggedErrorClass<NotFound>()("NotFound", { id: Schema.Number}) {}
console.log(new NotFound({ id: 1 })._tag) // => "NotFound"See also
Section titled “See also”- Defining errors —
ErrorClassandTaggedErrorClassfor schema-backed, yieldable errors. - Data — plain value classes (
Data.Class,Data.TaggedClass) when you do not need a schema. - Equal & Hash — the structural-equality traits that
Schema.Classinstances participate in.