Default Constructors
Every schema exposes constructors for creating values of its Type while applying constructor defaults and type-side checks.
Use make when a failure should throw, makeOption when you only need to know whether construction succeeded, and makeEffect when you need the SchemaError in the Effect error channel.
Example (Using a Refinement Default Constructor)
import { Schema } from "effect"
const schema = Schema.FiniteFromString.check( Schema.isBetween({ minimum: 1, maximum: 10 }),)
// The constructor only accepts numbersconsole.log(schema.make(5))// Output: 5
// This will throw an error because the number is outside the valid rangeconsole.log(schema.make(20))/*throws:Expected a number between 1 and 10*/Structs
Struct schemas allow you to define objects with specific fields and constraints. The make function can be used to create instances of a struct schema.
Example (Creating Struct Instances)
import { Schema } from "effect"
const Struct = Schema.Struct({ name: Schema.NonEmptyString,})
// Successful creationStruct.make({ name: "a" })
// This will throw an error because the name is emptyStruct.make({ name: "" })/*throwsExpected a value with a length of at least 1 at ["name"]*/When the input is already trusted, make can skip schema checks. This is not recommended for untrusted values.
Example (Skipping Checks)
import { Schema } from "effect"
const Struct = Schema.Struct({ name: Schema.NonEmptyString,})
// Skip checks when the input is already trustedStruct.make({ name: "" }, { disableChecks: true })Records
Record schemas allow you to define key-value mappings where the keys and values must meet specific criteria.
Example (Creating Record Instances)
import { Schema } from "effect"
const Record = Schema.Record(Schema.String, Schema.NonEmptyString)
// Successful creationRecord.make({ a: "a", b: "b" })
// This will throw an error because 'b' is emptyRecord.make({ a: "a", b: "" })/*throwsExpected a value with a length of at least 1 at ["b"]*/
// Skips checksRecord.make({ a: "a", b: "" }, { disableChecks: true })Filters
Filters allow you to define constraints on individual values.
Example (Using Filters to Enforce Ranges)
import { Schema } from "effect"
const MyNumber = Schema.Finite.check( Schema.isBetween({ minimum: 1, maximum: 10 }),)
// Successful creationconst n = MyNumber.make(5)
// This will throw an error because the number is outside the valid rangeMyNumber.make(20)/*throwsExpected a value between 1 and 10*/
// Skips checksMyNumber.make(20, { disableChecks: true })Branded Types
Branded schemas add metadata to a value to give it a more specific type, while still retaining its original type.
Example (Creating Branded Values)
import { Schema } from "effect"
const BrandedNumberSchema = Schema.Finite.pipe( Schema.check(Schema.isBetween({ minimum: 1, maximum: 10 })), Schema.brand("MyNumber"),)
// Successful creationconst n = BrandedNumberSchema.make(5)
// This will throw an error because the number is outside the valid rangeBrandedNumberSchema.make(20)/*throwsExpected a value between 1 and 10*/
// Skips checksBrandedNumberSchema.make(20, { disableChecks: true })When using default constructors, it is helpful to understand the type of value they produce.
For instance, in the BrandedNumberSchema example, the return type of the constructor is number & Brand<"MyNumber">. This indicates that the resulting value is a number with additional branding information, "MyNumber".
This behavior contrasts with the filter example, where the return type is simply number. Branding adds an extra layer of type information, which can assist in identifying and working with your data more effectively.
Error Handling in Constructors
make is appropriate when invalid constructor input is exceptional. If failure is expected, use makeOption or makeEffect instead.
makeOption returns Option.some on success and Option.none for schema issues. Use makeEffect when you need the full SchemaError.
Example (Constructing Without Throwing)
import { Option, Schema } from "effect"
const schema = Schema.FiniteFromString.check( Schema.isBetween({ minimum: 1, maximum: 10 }),)
schema.makeOption(5) // => Option.some(5)schema.makeOption(20) // => Option.none()
// Effect.Effect<number, SchemaError>const safely = schema.makeEffect(20)Setting Default Values
When creating objects, you might want to assign default values to certain fields to simplify object construction. The Schema.withConstructorDefault function lets you handle default values, making fields optional in the default constructor.
Example (Struct with Required Fields)
In this example, all fields are required when creating a new instance.
import { Schema } from "effect"
const Person = Schema.Struct({ name: Schema.NonEmptyString, age: Schema.Finite,})
// Both name and age must be providedconsole.log(Person.make({ name: "John", age: 30 }))/*Output: { name: 'John', age: 30 }*/Example (Struct with Default Value)
Here, the age field is optional because it has a default value of 0.
import { Effect, Schema } from "effect"
const Person = Schema.Struct({ name: Schema.NonEmptyString, age: Schema.Finite.pipe(Schema.withConstructorDefault(Effect.succeed(0))),})
// The age field is optional and defaults to 0console.log(Person.make({ name: "John" }))/*Output:{ name: 'John', age: 0 }*/
console.log(Person.make({ name: "John", age: 30 }))/*Output:{ name: 'John', age: 30 }*/Nested Defaults
Constructor defaults compose through nested schemas. Inner defaults are resolved after the value for the outer field is supplied or defaulted.
Example (Resolving Nested Defaults)
import { Effect, Schema } from "effect"
const Config = Schema.Struct({ web: Schema.Struct({ application_url: Schema.String.pipe( Schema.withConstructorDefault(Effect.succeed("http://localhost")), ), application_port: Schema.Finite, }).pipe( Schema.withConstructorDefault(Effect.succeed({ application_port: 3000 })), ),})
console.log(Config.make({}))/*Output:{ web: { application_url: 'http://localhost', application_port: 3000 }}*/Lazy Evaluation of Defaults
Defaults are lazily evaluated, meaning that a new instance of the default is generated every time the constructor is called:
Example (Lazy Evaluation of Defaults)
In this example, the timestamp field generates a new value for each instance.
import { Effect, Schema } from "effect"
const Person = Schema.Struct({ name: Schema.NonEmptyString, age: Schema.Finite.pipe(Schema.withConstructorDefault(Effect.succeed(0))), timestamp: Schema.Finite.pipe( Schema.withConstructorDefault(Effect.sync(() => new Date().getTime())), ),})
console.log(Person.make({ name: "name1" }))/*Example Output:{ age: 0, timestamp: 1714232909221, name: 'name1' }*/
console.log(Person.make({ name: "name2" }))/*Example Output:{ age: 0, timestamp: 1714232909227, name: 'name2' }*/Reusing Defaults Across Schemas
Default values are also “portable”, meaning that if you reuse the same property signature in another schema, the default is carried over:
Example (Reusing Defaults in Another Schema)
import { Effect, Schema } from "effect"
const Person = Schema.Struct({ name: Schema.NonEmptyString, age: Schema.Finite.pipe(Schema.withConstructorDefault(Effect.succeed(0))), timestamp: Schema.Finite.pipe( Schema.withConstructorDefault(Effect.sync(() => new Date().getTime())), ),})
const AnotherSchema = Schema.Struct({ foo: Schema.String, age: Person.fields.age,})
console.log(AnotherSchema.make({ foo: "bar" }))/*Output:{ foo: 'bar', age: 0 }*/Using Defaults in Classes
Default values can also be applied when working with the Class API, ensuring consistency across class-based schemas.
Example (Defaults in a Class)
import { Effect, Schema } from "effect"
class Person extends Schema.Class<Person>("Person")({ name: Schema.NonEmptyString, age: Schema.Finite.pipe(Schema.withConstructorDefault(Effect.succeed(0))), timestamp: Schema.Finite.pipe( Schema.withConstructorDefault(Effect.sync(() => new Date().getTime())), ),}) {}
console.log(new Person({ name: "name1" }))/*Example Output:Person { age: 0, timestamp: 1714400867208, name: 'name1' }*/
console.log(new Person({ name: "name2" }))/*Example Output:Person { age: 0, timestamp: 1714400867215, name: 'name2' }*/