Default Constructors
On this page
When working with data structures, it's advantageous to be able to construct values that align with a specific schema effortlessly.
For this purpose, we offer default constructors for several types of schemas, including Structs
, Records
, filters
, and brands
.
Remember, default constructors linked to a schema Schema<A, I, R>
specifically relate to the A
type rather than the I
type.
Structs
Example
ts
import {Schema } from "@effect/schema"constStruct =Schema .Struct ({name :Schema .NonEmptyString })// Successful creationStruct .make ({name : "a" })// This will cause an error because the name is emptyStruct .make ({name : "" })/*throwsParseError: { readonly name: NonEmptyString }└─ ["name"]└─ NonEmptyString└─ Predicate refinement failure└─ Expected NonEmptyString, actual ""*/
ts
import {Schema } from "@effect/schema"constStruct =Schema .Struct ({name :Schema .NonEmptyString })// Successful creationStruct .make ({name : "a" })// This will cause an error because the name is emptyStruct .make ({name : "" })/*throwsParseError: { readonly name: NonEmptyString }└─ ["name"]└─ NonEmptyString└─ Predicate refinement failure└─ Expected NonEmptyString, actual ""*/
There are scenarios where you might want to bypass validation during instantiation.
Although not typically recommended, @effect/schema
allows for this flexibility:
ts
// Bypasses validation, thus avoiding errorsStruct .make ({name : "" }, true)// or more explicitlyStruct .make ({name : "" }, {disableValidation : true })
ts
// Bypasses validation, thus avoiding errorsStruct .make ({name : "" }, true)// or more explicitlyStruct .make ({name : "" }, {disableValidation : true })
Records
Example
ts
import {Schema } from "@effect/schema"constRecord =Schema .Record ({key :Schema .String ,value :Schema .NonEmptyString })// Successful creationRecord .make ({a : "a",b : "b" })// This will cause 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 })
ts
import {Schema } from "@effect/schema"constRecord =Schema .Record ({key :Schema .String ,value :Schema .NonEmptyString })// Successful creationRecord .make ({a : "a",b : "b" })// This will cause 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
Example
ts
import {Schema } from "@effect/schema"constMyNumber =Schema .Number .pipe (Schema .between (1, 10))// Successful creationconstn =MyNumber .make (5)// This will cause 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 })
ts
import {Schema } from "@effect/schema"constMyNumber =Schema .Number .pipe (Schema .between (1, 10))// Successful creationconstn =MyNumber .make (5)// This will cause 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
Example
ts
import {Schema } from "@effect/schema"constBrandedNumberSchema =Schema .Number .pipe (Schema .between (1, 10),Schema .brand ("MyNumber"))// Successful creationconstn =BrandedNumberSchema .make (5)// This will cause 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 })
ts
import {Schema } from "@effect/schema"constBrandedNumberSchema =Schema .Number .pipe (Schema .between (1, 10),Schema .brand ("MyNumber"))// Successful creationconstn =BrandedNumberSchema .make (5)// This will cause 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 utilizing our default constructors, it's important to grasp the type of value they generate.
In the BrandedNumberSchema
example, the return type of the constructor is number & Brand<"MyNumber">
, indicating that the resulting value is a number with the added branding "MyNumber".
This differs from the filter example where the return type is simply number
.
The branding offers additional insights about the type, facilitating the identification and manipulation of your data.
Note that default constructors are "unsafe" in the sense that if the input does not conform to the schema, the constructor throws an error containing a description of what is wrong.
This is because the goal of default constructors is to provide a quick way to create compliant values (for example, for writing tests or configurations, or in any situation where it is assumed that the input passed to the constructors is valid and the opposite situation is exceptional).
To have a "safe" constructor, you can use Schema.validateEither
:
ts
import {Schema } from "@effect/schema"constMyNumber =Schema .Number .pipe (Schema .between (1, 10))constctor =Schema .validateEither (MyNumber )console .log (ctor (5))/*{ _id: 'Either', _tag: 'Right', right: 5 }*/console .log (ctor (20))/*{_id: 'Either',_tag: 'Left',left: {_id: 'ParseError',message: 'a number between 1 and 10\n' +'└─ Predicate refinement failure\n' +' └─ Expected a number between 1 and 10, actual 20'}}*/
ts
import {Schema } from "@effect/schema"constMyNumber =Schema .Number .pipe (Schema .between (1, 10))constctor =Schema .validateEither (MyNumber )console .log (ctor (5))/*{ _id: 'Either', _tag: 'Right', right: 5 }*/console .log (ctor (20))/*{_id: 'Either',_tag: 'Left',left: {_id: 'ParseError',message: 'a number between 1 and 10\n' +'└─ Predicate refinement failure\n' +' └─ Expected a number between 1 and 10, actual 20'}}*/
Setting Default Values
When constructing objects, it's common to want to assign default values to certain fields to simplify the creation of new instances.
The Schema.withConstructorDefault
function allows you to manage the optionality of a field in your default constructor.
Example: Without Default
ts
import {Schema } from "@effect/schema"constPerson =Schema .Struct ({name :Schema .NonEmptyString ,age :Schema .Number })// Both name and age are requiredPerson .make ({name : "John",age : 30 })
ts
import {Schema } from "@effect/schema"constPerson =Schema .Struct ({name :Schema .NonEmptyString ,age :Schema .Number })// Both name and age are requiredPerson .make ({name : "John",age : 30 })
Example: With Default
ts
import {Schema } from "@effect/schema"constPerson =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:{ age: 0, name: 'John' }*/
ts
import {Schema } from "@effect/schema"constPerson =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:{ age: 0, name: 'John' }*/
In the second example, notice how the age
field is now optional and defaults to 0
when not provided.
Defaults are lazily evaluated, meaning that a new instance of the default is generated every time the constructor is called:
ts
import {Schema } from "@effect/schema"constPerson =Schema .Struct ({name :Schema .NonEmptyString ,age :Schema .Number .pipe (Schema .propertySignature ,Schema .withConstructorDefault (() => 0)),timestamp :Schema .Number .pipe (Schema .propertySignature ,Schema .withConstructorDefault (() => newDate ().getTime ()))})console .log (Person .make ({name : "name1" }))// { age: 0, timestamp: 1714232909221, name: 'name1' }console .log (Person .make ({name : "name2" }))// { age: 0, timestamp: 1714232909227, name: 'name2' }
ts
import {Schema } from "@effect/schema"constPerson =Schema .Struct ({name :Schema .NonEmptyString ,age :Schema .Number .pipe (Schema .propertySignature ,Schema .withConstructorDefault (() => 0)),timestamp :Schema .Number .pipe (Schema .propertySignature ,Schema .withConstructorDefault (() => newDate ().getTime ()))})console .log (Person .make ({name : "name1" }))// { age: 0, timestamp: 1714232909221, name: 'name1' }console .log (Person .make ({name : "name2" }))// { age: 0, timestamp: 1714232909227, name: 'name2' }
Note how the timestamp
field varies.
Default values are also "portable", meaning that if you reuse the same property signature in another schema, the default is carried over:
ts
import {Schema } from "@effect/schema"constPerson =Schema .Struct ({name :Schema .NonEmptyString ,age :Schema .Number .pipe (Schema .propertySignature ,Schema .withConstructorDefault (() => 0)),timestamp :Schema .Number .pipe (Schema .propertySignature ,Schema .withConstructorDefault (() => newDate ().getTime ()))})constAnotherSchema =Schema .Struct ({foo :Schema .String ,age :Person .fields .age })console .log (AnotherSchema .make ({foo : "bar" })) // => { foo: 'bar', age: 0 }
ts
import {Schema } from "@effect/schema"constPerson =Schema .Struct ({name :Schema .NonEmptyString ,age :Schema .Number .pipe (Schema .propertySignature ,Schema .withConstructorDefault (() => 0)),timestamp :Schema .Number .pipe (Schema .propertySignature ,Schema .withConstructorDefault (() => newDate ().getTime ()))})constAnotherSchema =Schema .Struct ({foo :Schema .String ,age :Person .fields .age })console .log (AnotherSchema .make ({foo : "bar" })) // => { foo: 'bar', age: 0 }
Defaults can also be applied using the Class
API:
ts
import {Schema } from "@effect/schema"classPerson extendsSchema .Class <Person >("Person")({name :Schema .NonEmptyString ,age :Schema .Number .pipe (Schema .propertySignature ,Schema .withConstructorDefault (() => 0)),timestamp :Schema .Number .pipe (Schema .propertySignature ,Schema .withConstructorDefault (() => newDate ().getTime ()))}) {}console .log (newPerson ({name : "name1" }))// Person { age: 0, timestamp: 1714400867208, name: 'name1' }console .log (newPerson ({name : "name2" }))// Person { age: 0, timestamp: 1714400867215, name: 'name2' }
ts
import {Schema } from "@effect/schema"classPerson extendsSchema .Class <Person >("Person")({name :Schema .NonEmptyString ,age :Schema .Number .pipe (Schema .propertySignature ,Schema .withConstructorDefault (() => 0)),timestamp :Schema .Number .pipe (Schema .propertySignature ,Schema .withConstructorDefault (() => newDate ().getTime ()))}) {}console .log (newPerson ({name : "name1" }))// Person { age: 0, timestamp: 1714400867208, name: 'name1' }console .log (newPerson ({name : "name2" }))// Person { age: 0, timestamp: 1714400867215, name: 'name2' }