Default Constructors
When working with data structures, it can be helpful to create values that conform to a schema with minimal effort.
For this purpose, the Schema module provides default constructors for various schema types, including Structs, Records, filters, and brands.
Default constructors are unsafe, meaning they throw an error if the input does not conform to the schema. If you need a safer alternative, consider using Schema.validateEither, which returns a result indicating success or failure instead of throwing an error.
Example (Using a Refinement Default Constructor)
import { Schema } from "effect"
const schema = Schema.NumberFromString.pipe(Schema.between(1, 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:ParseError: between(1, 10)└─ Predicate refinement failure └─ Expected a number between 1 and 10, actual 20*/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: "" })/*throwsParseError: { readonly name: NonEmptyString }└─ ["name"] └─ NonEmptyString └─ Predicate refinement failure └─ Expected NonEmptyString, actual ""*/In some cases, you might need to bypass validation. While not recommended in most scenarios, make provides an option to disable validation.
Example (Bypassing Validation)
import { Schema } from "effect"
const Struct = Schema.Struct({ name: Schema.NonEmptyString,})
// Bypass validation during instantiationStruct.make({ name: "" }, true)
// Or use the `disableValidation` option explicitlyStruct.make({ name: "" }, { disableValidation: 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({ key: Schema.String, value: Schema.NonEmptyString,})
// Successful creationRecord.make({ a: "a", b: "b" })
// This will throw an error because 'b' is emptyRecord.make({ a: "a", b: "" })/*throwsParseError: { readonly [x: string]: NonEmptyString }└─ ["b"] └─ NonEmptyString └─ Predicate refinement failure └─ Expected NonEmptyString, actual ""*/
// Bypasses validationRecord.make({ a: "a", b: "" }, { disableValidation: true })Filters
Filters allow you to define constraints on individual values.
Example (Using Filters to Enforce Ranges)
import { Schema } from "effect"
const MyNumber = Schema.Number.pipe(Schema.between(1, 10))
// Successful creationconst n = MyNumber.make(5)
// This will throw an error because the number is outside the valid rangeMyNumber.make(20)/*throwsParseError: a number between 1 and 10└─ Predicate refinement failure └─ Expected a number between 1 and 10, actual 20*/
// Bypasses validationMyNumber.make(20, { disableValidation: 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.Number.pipe(Schema.between(1, 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)/*throwsParseError: a number between 1 and 10 & Brand<"MyNumber">└─ Predicate refinement failure └─ Expected a number between 1 and 10 & Brand<"MyNumber">, actual 20*/
// Bypasses validationBrandedNumberSchema.make(20, { disableValidation: 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
Default constructors are considered “unsafe” because they throw an error if the input does not conform to the schema. This error includes a detailed description of what went wrong. The intention behind default constructors is to provide a straightforward way to create valid values, such as for tests or configurations, where invalid inputs are expected to be exceptional cases.
If you need a “safe” constructor that does not throw errors but instead returns a result indicating success or failure, you can use Schema.validateEither.
Example (Using Schema.validateEither for Safe Validation)
import { Schema } from "effect"
const schema = Schema.NumberFromString.pipe(Schema.between(1, 10))
// Create a safe constructor that validates an unknown inputconst safeMake = Schema.validateEither(schema)
// Valid input returns a Right valueconsole.log(safeMake(5))/*Output:{ _id: 'Either', _tag: 'Right', right: 5 }*/
// Invalid input returns a Left value with detailed error informationconsole.log(safeMake(20))/*Output:{ _id: 'Either', _tag: 'Left', left: { _id: 'ParseError', message: 'between(1, 10)\n' + '└─ Predicate refinement failure\n' + ' └─ Expected a number between 1 and 10, actual 20' }}*/
// This will throw an error because it's unsafeschema.make(20)/*throws:ParseError: between(1, 10)└─ Predicate refinement failure └─ Expected a number between 1 and 10, actual 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.Number,})
// 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 { Schema } from "effect"
const Person = Schema.Struct({ name: Schema.NonEmptyString, age: Schema.Number.pipe( Schema.propertySignature, Schema.withConstructorDefault(() => 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 Structs and Shallow Defaults
Default values in schemas are shallow, meaning that defaults defined in nested structs do not automatically propagate to the top-level constructor.
Example (Shallow Defaults in Nested Structs)
import { Schema } from "effect"
const Config = Schema.Struct({ // Define a nested struct with a default value web: Schema.Struct({ application_url: Schema.String.pipe( Schema.propertySignature, Schema.withConstructorDefault(() => "http://localhost"), ), application_port: Schema.Number, }),})
// This will cause a type error because `application_url`// is missing in the nested struct// @errors: 2741Config.make({ web: { application_port: 3000 } })This behavior occurs because the Schema interface does not include a type parameter to carry over default constructor types from nested structs.
To work around this limitation, extract the constructor for the nested struct and apply it to its fields directly. This ensures that the nested defaults are respected.
Example (Using Nested Struct Constructors)
import { Schema } from "effect"
const Config = Schema.Struct({ web: Schema.Struct({ application_url: Schema.String.pipe( Schema.propertySignature, Schema.withConstructorDefault(() => "http://localhost"), ), application_port: Schema.Number, }),})
// Extract the nested struct constructorconst { web: Web } = Config.fields
// Use the constructor for the nested structconsole.log(Config.make({ web: Web.make({ application_port: 3000 }) }))/*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 { Schema } from "effect"
const Person = Schema.Struct({ name: Schema.NonEmptyString, age: Schema.Number.pipe( Schema.propertySignature, Schema.withConstructorDefault(() => 0), ), timestamp: Schema.Number.pipe( Schema.propertySignature, Schema.withConstructorDefault(() => 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 { Schema } from "effect"
const Person = Schema.Struct({ name: Schema.NonEmptyString, age: Schema.Number.pipe( Schema.propertySignature, Schema.withConstructorDefault(() => 0), ), timestamp: Schema.Number.pipe( Schema.propertySignature, Schema.withConstructorDefault(() => 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 { Schema } from "effect"
class Person extends Schema.Class<Person>("Person")({ name: Schema.NonEmptyString, age: Schema.Number.pipe( Schema.propertySignature, Schema.withConstructorDefault(() => 0), ), timestamp: Schema.Number.pipe( Schema.propertySignature, Schema.withConstructorDefault(() => 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' }*/