Skip to content
LogoLogo

Default Constructors

Schema constructors work as in Effect and operate on decoded values. They apply constructor defaults and type-side checks without running the codec's encoded-to-decoded conversion. In local Effect 4.0.5, makeEffect fails with a raw schema issue. Plain builders follow the trusted call rules.

Constructing structs and records

// run: Console.log(JSON.stringify(makeLabels()))
import { Console } from "effect"
const Label = Schema.Struct({ text: Schema.NonEmptyString })
const LabelsById = Schema.Record(Schema.String, Schema.NonEmptyString)
 
function makeLabels() {
  const label = Label.makeEffect({ text: "Keep upright" })
  const byId = LabelsById.makeEffect({ handling: label.text })
  return { label, byId }
}
{"label":{"text":"Keep upright"},"byId":{"handling":"Keep upright"}}

Every schema exposes make, makeOption, and makeEffect. A constructor takes the schema's constructor input and returns its decoded type. FiniteFromString.makeEffect(6) constructs a number; its decoder instead accepts text. Struct and record constructors validate fields and entries.

Applying filters and brands during construction

// run: Console.log(makeCaseCount(6)); try { makeCaseCount(0) } catch (issue) { Console.log(issue._tag) }
const CaseCount = Schema.FiniteFromString.check(
  Schema.isInt(), Schema.isBetween({ minimum: 1, maximum: 48 })
).pipe(Schema.brand("CaseCount"))
 
function makeCaseCount(value: number) {
  return CaseCount.makeEffect(value)
}
6
Composite

Filters run during construction as well as parsing. A filter leaves the static type unchanged; a brand returns a branded number while keeping the runtime number representation. { disableChecks: true } deliberately skips checks, but does not decode an encoded input or establish that the value is valid.

Handling constructor failures

// run: Console.log(constructOrExplain(6)); Console.log(constructAsOption(0).isNone()); try { constructWithThrowingApi(0) } catch (error) { Console.log("Construction rejected") }
function constructOrExplain(value: number) {
  try {
    return String(CaseCount.makeEffect(value))
  } catch (issue) {
    return try((issue) => SchemaIssue.makeFormatterDefault()(issue), issue)
  }
}
 
function constructAsOption(value: number) {
  return try((value) => CaseCount.makeOption(value), value)
}
 
function constructWithThrowingApi(value: number) {
  return try((value) => CaseCount.make(value), value)
}
6
true
Construction rejected

makeEffect fails with SchemaIssue.Issue, not the SchemaError wrapper returned by Schema decoders. Format the issue directly or translate it to an application error. makeOption discards schema-validation details, while make throws a generic validation Error with the issue in cause. Both synchronous APIs can throw for non-schema causes or asynchronous work, so their explicit boundaries remain useful.

Setting nested and lazy defaults

// run: const first = makePacking(); const second = makePacking(); Console.log(JSON.stringify(first)); Console.log(first.labels === second.labels)
const Packing = Schema.Struct({
  handling: Schema.String.pipe(Schema.withConstructorDefault(() => "standard")),
  dimensions: Schema.Struct({
    unit: Schema.String.pipe(Schema.withConstructorDefault(() => "cm")),
    width: Schema.Finite
  }).pipe(Schema.withConstructorDefault(() => ({ width: 10 }))),
  labels: Schema.Array(Schema.String).pipe(Schema.withConstructorDefault(() => []))
})
 
function makePacking() {
  return Packing.makeEffect({})
}
// { handling: "standard", dimensions: { unit: "cm", width: 10 }, labels: [] }
{"handling":"standard","dimensions":{"unit":"cm","width":10},"labels":[]}
false

Pass a program to withConstructorDefault; the constructor runs it only when the field needs a default. An outer default supplies nested constructor input, after which inner defaults fill their own fields. A fresh [] is created each time the labels default runs, rather than sharing one mutable array.

The default program may fail with schema issues but cannot require services. make and makeOption need defaults that complete synchronously; use makeEffect if a default pauses. Read service-dependent values in the enclosing program and pass them as explicit fields instead.

Reusing defaults

// run: Console.log(JSON.stringify(makeReturnPacking()))
const ReturnPacking = Schema.Struct({
  reason: Schema.String,
  handling: Packing.fields.handling,
  labels: Packing.fields.labels
})
 
function makeReturnPacking() {
  return ReturnPacking.makeEffect({ reason: "inspection" })
}
{"reason":"inspection","handling":"standard","labels":[]}

Defaults belong to field descriptions. Reusing a field carries its constructor default into the new struct. A documented default annotation is metadata only, and a constructor default does not make decoding accept a missing field; add a decoding default separately when both behaviors are required.

Initializing schema-backed classes

// run: const value = createPackingRequest("pkg-94"); Console.log(value instanceof PackingRequest); Console.log(JSON.stringify(value))
class PackingRequest extends Schema.Class<PackingRequest>("PackingRequest")({
  reference: Schema.String,
  handling: Packing.fields.handling,
  labels: Packing.fields.labels
}) {}
 
function createPackingRequest(reference: string) {
  return PackingRequest.makeEffect({ reference })
}
true
{"reference":"pkg-94","handling":"standard","labels":[]}

Classes use the same default-bearing field descriptions as structs. The result is a validated instance with its class behavior. new PackingRequest(...) also applies defaults. Exceptions escaping into Effect code are defects unless an explicit try hatch gives them an expected error type. A plain-contract function keeps JavaScript catch and receives every exception as unknown (D260); new itself never runs a program.


The matching Effect page: effect.website/docs/v4/schema/default-constructors