Class APIs
Schema classes work in Effeck as in Effect. A class can validate its fields, act as a codec, and expose domain behavior on its instances. Use one when methods or class identity help the model; use Struct for plain records. Plain builders follow the trusted call rules.
Declaring validated classes
class Package extends Schema.Class<Package>("Package")({
reference: Schema.NonEmptyString,
pieces: Schema.FiniteFromString
}) {}
function readPackage(input: unknown) {
return Schema.decodeUnknownEffect(Package)(input)
}
// Package!Schema.SchemaError!Supply the class itself as the Self type, a stable identifier, and its fields. The class extends a generated base and is an ordinary data class, not an Effeck service. The identifier is available as Package.identifier and helps Effect recognize class instances across module reloads.
Constructing and checking instances
function constructPackage(reference: string, pieces: number) {
return Package.makeEffect({ reference, pieces })
}
function constructWithNew(reference: string) {
return try((reference) => new Package({ reference, pieces: 1 }), reference)
}Construction takes decoded fields, so pieces is a number even though decoding takes a string. new Package(...) and Package.make(...) apply defaults and type-side checks and throw for invalid construction; an explicit boundary accounts for those exceptions. makeEffect reports raw SchemaIssue.Issue failures in local Effect 4.0.5. An empty-field class can be constructed without an argument.
Pass a checked Schema.Struct instead of a fields object when the class needs a cross-field invariant. Its checks apply during construction and codec use.
Bypassing checks deliberately
function fromVerifiedFields(reference: string, pieces: number) {
return try((reference, pieces) => new Package({ reference, pieces }, { disableChecks: true }), reference, pieces)
}disableChecks skips validation. It does not decode fields or prove an untrusted value valid. Use it only when the input already satisfies the decoded model; constructors still have their normal default-handling behavior.
Defining structural equality
class Container extends Schema.Class<Container>("Container")({
labels: Schema.Array(Schema.String)
}) {}
function sameContainer() {
const first = Container.makeEffect({ labels: ["cold", "sealed"] })
const second = Container.makeEffect({ labels: ["cold", "sealed"] })
return Equal.equals(first, second) // true
}Schema-backed class instances support Effect's structural equality, including nested arrays and objects. JavaScript === still compares object identity. Keep fields stable when values participate in hash collections.
Adding getters and methods
class LabeledPackage extends Schema.Class<LabeledPackage>("LabeledPackage")({
reference: Schema.NonEmptyString
}) {
get displayReference() {
return this.reference.toUpperCase()
}
describe() {
return `Package ${this.reference}`
}
announce() {
log(this.describe())
}
}
function announcePackage() {
const item = LabeledPackage.makeEffect({ reference: "pkg-94" })
item.announce()
return item.displayReference
}Getters derive synchronous values from fields. Methods follow ordinary Effeck call rules and can run effects; calling announce logs before returning. Keep getters pure so formatting, checks, and compiled validators can read properties safely.
Using classes as codecs
const Packages = Schema.Array(Package)
function packageWireRoundTrip(input: unknown) {
const packages = Schema.decodeUnknownEffect(Packages)(input)
return Schema.encodeEffect(Packages)(packages)
}A class schema is a transformation: decoding a plain object creates an instance, and encoding an instance produces a plain object with encoded fields. Classes fit anywhere a schema is expected, including arrays and nested structs. Package.fields exposes the original field descriptions for reuse.
Annotating classes
class Dock extends Schema.Class<Dock>("Dock")(
{ number: Schema.Int },
{ title: "Loading dock", description: "A numbered dispatch location" }
) {}Pass annotations after the fields or struct. The class identifier also becomes its default identifier annotation. Metadata documents the model without adding checks or changing construction defaults.
Modeling recursive classes
interface CrateWire {
count: string
children: ReadonlyArray<CrateWire>
}
class Crate extends Schema.Class<Crate>("Crate")({
count: Schema.FiniteFromString,
children: Schema.Array(
Schema.suspend((): Schema.Codec<Crate, CrateWire>!! => Crate)
)
}) {}
function readCrate(input: unknown) {
return Schema.decodeUnknownEffect(Crate)(input)
}Suspend the recursive edge and write the callback's full contract to break the inference cycle. It returns the codec as data and compiles to a plain JavaScript callback. Specify a separate encoded interface when fields transform. For mutual recursion, suspend the reference to the class declared later and state the appropriate decoded and encoded types on each recursive edge.
Creating tagged variants and extending classes
class Packed extends Schema.TaggedClass<Packed>()("Packed", {
reference: Schema.String
}) {}
class MissingPackage extends Schema.TaggedError<MissingPackage>()("MissingPackage", {
reference: Schema.String
}) {}
class InsuredPackage extends Package.extend<InsuredPackage>("InsuredPackage")({
insuredValue: Schema.Finite
}) {
summary() {
return `${this.reference}: ${this.insuredValue}`
}
}
function reportMissing(reference: string) {
throw MissingPackage.makeEffect({ reference })
}
// never!MissingPackage | SchemaIssue.Issue!TaggedClass adds _tag with a constructor default. Schema.TaggedError also supplies Error behavior and a serializable schema; the constructed instance is data until thrown or called. new constructs it without running it; throw it for an expected failure. The example also retains the constructor's issue channel. For errors that do not need serialization, use the global TaggedError instead.
extend adds fields while preserving inherited behavior. Give each extension its own identifier. Schema data-class inheritance is separate from service declarations, whose implementations use object literals or implements.