Skip to content

Classes

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 })"

A plain Schema.Struct produces anonymous objects. A Schema.Class gives you:

  • Validated constructionnew 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) // true

Because the class extends Data.Class, instances compare by value rather than by reference. This makes class instances safe to use as keys in HashMap, members of a HashSet, or anywhere Effect relies on structural equality.

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 values
console.log(a === b) // false — different references

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

Schema.TaggedClass adds a _tag literal field automatically, so instances participate in tagged-union pattern matching. Note the extra () — the tag is 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) // 5

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"