Basic Usage
On this page
Cheatsheet
Typescript Type | Description / Notes | Schema / Combinator / Example |
---|---|---|
null | S.Null | |
undefined | S.Undefined | |
string | S.String | |
number | S.Number | |
boolean | S.Boolean | |
symbol | S.SymbolFromSelf / S.Symbol | |
BigInt | S.BigIntFromSelf / S.BigInt | |
unknown | S.Unknown | |
any | S.Any | |
never | S.Never | |
object | S.Object | |
unique symbol | S.UniqueSymbolFromSelf | |
"a" , 1 , true | type literals | S.Literal("a") , S.Literal(1) , S.Literal(true) |
a${string} | template literals | S.TemplateLiteral("a", S.String) |
{ readonly a: string, readonly b?: number| undefined } | structs | S.Struct({ a: S.String, b: S.optional(S.Number) }) |
Record<A, B> | records | S.Record({ key: A, value: B }) |
readonly [string, number] | tuples | S.Tuple(S.String, S.Number) |
ReadonlyArray<string> | arrays | S.Array(S.String) |
A | B | unions | S.Union(A, B) |
A & B | intersections of non-overlapping structs | S.extend(A, B) |
Record<A, B> & Record<C, D> | intersections of non-overlapping records | S.extend(S.Record({ key: A, value: B }), S.Record({ key: C, value: D })) |
type A = { a: A | null } | recursive types | S.Struct({ a: S.Union(S.Null, S.suspend(() => self)) }) |
keyof A | S.keyof(A) | |
Partial<A> | S.partial(A) | |
Required<A> | S.required(A) |
Here are the primitive schemas provided by the @effect/schema/Schema
module:
Primitives
These primitive schemas are building blocks for creating more complex schemas to describe your data structures.
ts
import {Schema } from "@effect/schema"Schema .String // Schema<string>Schema .Number // Schema<number>Schema .Boolean // Schema<boolean>Schema .BigIntFromSelf // Schema<BigInt>Schema .SymbolFromSelf // Schema<symbol>Schema .Object // Schema<object>Schema .Undefined // Schema<undefined>Schema .Void // Schema<void>Schema .Any // Schema<any>Schema .Unknown // Schema<unknown>Schema .Never // Schema<never>
ts
import {Schema } from "@effect/schema"Schema .String // Schema<string>Schema .Number // Schema<number>Schema .Boolean // Schema<boolean>Schema .BigIntFromSelf // Schema<BigInt>Schema .SymbolFromSelf // Schema<symbol>Schema .Object // Schema<object>Schema .Undefined // Schema<undefined>Schema .Void // Schema<void>Schema .Any // Schema<any>Schema .Unknown // Schema<unknown>Schema .Never // Schema<never>
Literals
Literals represent specific values that are directly specified.
ts
import {Schema } from "@effect/schema"Schema .Null // same as S.Literal(null)Schema .Literal ("a")Schema .Literal ("a", "b", "c") // union of literalsSchema .Literal (1)Schema .Literal (2n) // BigInt literalSchema .Literal (true)
ts
import {Schema } from "@effect/schema"Schema .Null // same as S.Literal(null)Schema .Literal ("a")Schema .Literal ("a", "b", "c") // union of literalsSchema .Literal (1)Schema .Literal (2n) // BigInt literalSchema .Literal (true)
Exposed Values
You can access the literals of a literal schema:
ts
import {Schema } from "@effect/schema"constschema =Schema .Literal ("a", "b")// Accesses the literalsconstliterals =schema .literals
ts
import {Schema } from "@effect/schema"constschema =Schema .Literal ("a", "b")// Accesses the literalsconstliterals =schema .literals
The pickLiteral Utility
We can also use Schema.pickLiteral
with a literal schema to narrow down the possible values:
ts
import {Schema } from "@effect/schema"Schema .Literal ("a", "b", "c").pipe (Schema .pickLiteral ("a", "b")) // same as S.Literal("a", "b")
ts
import {Schema } from "@effect/schema"Schema .Literal ("a", "b", "c").pipe (Schema .pickLiteral ("a", "b")) // same as S.Literal("a", "b")
Sometimes, we need to reuse a schema literal in other parts of our code. Let's see an example:
ts
import {Schema } from "@effect/schema"constFruitId =Schema .Number // the source of truth regarding the Fruit categoryconstFruitCategory =Schema .Literal ("sweet", "citrus", "tropical")constFruit =Schema .Struct ({id :FruitId ,category :FruitCategory })// Here, we want to reuse our FruitCategory definition to create a subtype of FruitconstSweetAndCitrusFruit =Schema .Struct ({fruitId :FruitId ,category :FruitCategory .pipe (Schema .pickLiteral ("sweet", "citrus"))/*By using pickLiteral from the FruitCategory, we ensure that the values selectedare those defined in the category definition above.If we remove "sweet" from the FruitCategory definition, TypeScript will notify us.*/})
ts
import {Schema } from "@effect/schema"constFruitId =Schema .Number // the source of truth regarding the Fruit categoryconstFruitCategory =Schema .Literal ("sweet", "citrus", "tropical")constFruit =Schema .Struct ({id :FruitId ,category :FruitCategory })// Here, we want to reuse our FruitCategory definition to create a subtype of FruitconstSweetAndCitrusFruit =Schema .Struct ({fruitId :FruitId ,category :FruitCategory .pipe (Schema .pickLiteral ("sweet", "citrus"))/*By using pickLiteral from the FruitCategory, we ensure that the values selectedare those defined in the category definition above.If we remove "sweet" from the FruitCategory definition, TypeScript will notify us.*/})
In this example, FruitCategory
serves as the source of truth for the categories of fruits. We reuse it to create a subtype of Fruit
called SweetAndCitrusFruit
, ensuring that only the categories defined in FruitCategory
are allowed.
Template literals
In TypeScript, template literals allow you to embed expressions within string literals.
The Schema.TemplateLiteral
constructor allows you to create a schema for these template literal types.
Here's how you can use it:
ts
import {Schema } from "@effect/schema"// This creates a TemplateLiteral of type `a${string}`Schema .TemplateLiteral ("a",Schema .String )// This creates a TemplateLiteral of type `https://${string}.com` or `https://${string}.net`Schema .TemplateLiteral ("https://",Schema .String ,".",Schema .Literal ("com", "net"))
ts
import {Schema } from "@effect/schema"// This creates a TemplateLiteral of type `a${string}`Schema .TemplateLiteral ("a",Schema .String )// This creates a TemplateLiteral of type `https://${string}.com` or `https://${string}.net`Schema .TemplateLiteral ("https://",Schema .String ,".",Schema .Literal ("com", "net"))
Let's look at a more complex example. Suppose you have two sets of locale IDs for emails and footers:
ts
import {Schema } from "@effect/schema"// example from https://www.typescriptlang.org/docs/handbook/2/template-literal-types.htmlconstEmailLocaleIDs =Schema .Literal ("welcome_email", "email_heading")constFooterLocaleIDs =Schema .Literal ("footer_title", "footer_sendoff")
ts
import {Schema } from "@effect/schema"// example from https://www.typescriptlang.org/docs/handbook/2/template-literal-types.htmlconstEmailLocaleIDs =Schema .Literal ("welcome_email", "email_heading")constFooterLocaleIDs =Schema .Literal ("footer_title", "footer_sendoff")
You can use the Schema.TemplateLiteral
constructor to create a schema that combines these IDs:
ts
// This creates a TemplateLiteral of type "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"Schema .TemplateLiteral (Schema .Union (EmailLocaleIDs ,FooterLocaleIDs ), "_id")
ts
// This creates a TemplateLiteral of type "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"Schema .TemplateLiteral (Schema .Union (EmailLocaleIDs ,FooterLocaleIDs ), "_id")
The Schema.TemplateLiteral
constructor supports the following types of spans:
Schema.String
Schema.Number
- Literals:
string | number | boolean | null | bigint
. These can be either wrapped bySchema.Literal
or used directly - Unions of the above types
TemplateLiteralParser
The Schema.TemplateLiteral
constructor, while useful as a simple validator, only verifies that an input conforms to a specific string pattern by converting template literal definitions into regular expressions. Similarly, Schema.pattern
employs regular expressions directly for the same purpose. Post-validation, both methods require additional manual parsing to convert the validated string into a usable data format.
To address these limitations and eliminate the need for manual post-validation parsing, the new TemplateLiteralParser
API has been developed. It not only validates the input format but also automatically parses it into a more structured and type-safe output, specifically into a tuple format.
This new approach enhances developer productivity by reducing boilerplate code and simplifying the process of working with complex string inputs.
Example
ts
import {Schema } from "@effect/schema"// const schema: Schema.Schema<readonly [number, "a", string], `${string}a${string}`, never>constschema =Schema .TemplateLiteralParser (Schema .NumberFromString ,"a",Schema .NonEmptyString )console .log (Schema .decodeEither (schema )("100afoo"))// { _id: 'Either', _tag: 'Right', right: [ 100, 'a', 'foo' ] }console .log (Schema .encode (schema )([100, "a", "foo"]))// { _id: 'Either', _tag: 'Right', right: '100afoo' }
ts
import {Schema } from "@effect/schema"// const schema: Schema.Schema<readonly [number, "a", string], `${string}a${string}`, never>constschema =Schema .TemplateLiteralParser (Schema .NumberFromString ,"a",Schema .NonEmptyString )console .log (Schema .decodeEither (schema )("100afoo"))// { _id: 'Either', _tag: 'Right', right: [ 100, 'a', 'foo' ] }console .log (Schema .encode (schema )([100, "a", "foo"]))// { _id: 'Either', _tag: 'Right', right: '100afoo' }
Unique Symbols
ts
import {Schema } from "@effect/schema"constmySymbol =Symbol .for ("mysymbol")// const mySymbolSchema: S.Schema<typeof mySymbol>constmySymbolSchema =Schema .UniqueSymbolFromSelf (mySymbol )
ts
import {Schema } from "@effect/schema"constmySymbol =Symbol .for ("mysymbol")// const mySymbolSchema: S.Schema<typeof mySymbol>constmySymbolSchema =Schema .UniqueSymbolFromSelf (mySymbol )
Filters
Using the Schema.filter
function, developers can define custom validation logic that goes beyond basic type checks, allowing for in-depth control over the data conformity process. This function applies a predicate to data, and if the data fails the predicate's condition, a custom error message can be returned.
For effectful filters, see filterEffect.
Example: Simple Validation
ts
import {Schema } from "@effect/schema"constLongString =Schema .String .pipe (Schema .filter ((s ) =>s .length >= 10 || "a string at least 10 characters long"))// stringtypeLongString = typeofLongString .Type console .log (Schema .decodeUnknownSync (LongString )("a"))/*throws:ParseError: { string | filter }└─ Predicate refinement failure└─ a string at least 10 characters long*/
ts
import {Schema } from "@effect/schema"constLongString =Schema .String .pipe (Schema .filter ((s ) =>s .length >= 10 || "a string at least 10 characters long"))// stringtypeLongString = typeofLongString .Type console .log (Schema .decodeUnknownSync (LongString )("a"))/*throws:ParseError: { string | filter }└─ Predicate refinement failure└─ a string at least 10 characters long*/
Please note that the use of filters do not alter the Type
of the schema.
They only serve to add additional constraints to the parsing process. If you
intend to modify the Type
, consider using Branded types.
Predicate Function Structure
The predicate for a filter is defined as follows:
ts
type Predicate = (a: A,options: ParseOptions,self: AST.Refinement) => FilterReturnType
ts
type Predicate = (a: A,options: ParseOptions,self: AST.Refinement) => FilterReturnType
where
ts
interface FilterIssue {readonly path: ReadonlyArray<PropertyKey>readonly issue: string | ParseResult.ParseIssue}type FilterOutput =| undefined| boolean| string| ParseResult.ParseIssue| FilterIssuetype FilterReturnType = FilterOutput | ReadonlyArray<FilterOutput>
ts
interface FilterIssue {readonly path: ReadonlyArray<PropertyKey>readonly issue: string | ParseResult.ParseIssue}type FilterOutput =| undefined| boolean| string| ParseResult.ParseIssue| FilterIssuetype FilterReturnType = FilterOutput | ReadonlyArray<FilterOutput>
Filter predicates can return several types of values, each with specific implications:
true
: The data satisfies the filter's condition.false
orundefined
: The filter is not satisfied, and no specific error message is provided.string
: The filter fails, and the provided string is used as the default error message.ParseResult.ParseIssue
: The filter fails with a detailed error structure.FilterIssue
: Allows specifying detailed error paths and messages, enhancing error specificity.
An array can be returned if multiple issues need to be reported, allowing for complex validations that may have multiple points of failure.
Annotations
It's beneficial to embed as much metadata as possible within the schema. This metadata can include identifiers, JSON schema specifications, and descriptive text to facilitate later analysis and understanding of the schema's purpose and constraints.
Example
ts
import {Schema } from "@effect/schema"constLongString =Schema .String .pipe (Schema .filter ((s ) =>s .length >= 10 ?undefined : "a string at least 10 characters long",{identifier : "LongString",jsonSchema : {minLength : 10 },description :"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"}))console .log (Schema .decodeUnknownSync (LongString )("a"))/*throws:ParseError: { string | filter }└─ Predicate refinement failure└─ a string at least 10 characters long*/
ts
import {Schema } from "@effect/schema"constLongString =Schema .String .pipe (Schema .filter ((s ) =>s .length >= 10 ?undefined : "a string at least 10 characters long",{identifier : "LongString",jsonSchema : {minLength : 10 },description :"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"}))console .log (Schema .decodeUnknownSync (LongString )("a"))/*throws:ParseError: { string | filter }└─ Predicate refinement failure└─ a string at least 10 characters long*/
Specifying Error Paths
It's possible to specify an error path along with the message, which enhances error specificity and is particularly beneficial for integration with tools like react-hook-form.
Example
ts
import {ArrayFormatter ,Schema } from "@effect/schema"import {Either } from "effect"constPassword =Schema .Trim .pipe (Schema .minLength (1))constMyForm =Schema .Struct ({password :Password ,confirm_password :Password }).pipe (Schema .filter ((input ) => {if (input .password !==input .confirm_password ) {return {path : ["confirm_password"],message : "Passwords do not match"}}}))console .log (JSON .stringify (Schema .decodeUnknownEither (MyForm )({password : "abc",confirm_password : "d"}).pipe (Either .mapLeft ((error ) =>ArrayFormatter .formatErrorSync (error ))),null,2))/*"_id": "Either","_tag": "Left","left": [{"_tag": "Type","path": ["confirm_password"],"message": "Passwords do not match"}]}*/
ts
import {ArrayFormatter ,Schema } from "@effect/schema"import {Either } from "effect"constPassword =Schema .Trim .pipe (Schema .minLength (1))constMyForm =Schema .Struct ({password :Password ,confirm_password :Password }).pipe (Schema .filter ((input ) => {if (input .password !==input .confirm_password ) {return {path : ["confirm_password"],message : "Passwords do not match"}}}))console .log (JSON .stringify (Schema .decodeUnknownEither (MyForm )({password : "abc",confirm_password : "d"}).pipe (Either .mapLeft ((error ) =>ArrayFormatter .formatErrorSync (error ))),null,2))/*"_id": "Either","_tag": "Left","left": [{"_tag": "Type","path": ["confirm_password"],"message": "Passwords do not match"}]}*/
This allows the error to be directly associated with the confirm_password
field, improving clarity for the end-user.
The use of ArrayFormatter
translates the error details into a more
comprehensible format. For further details, see
ArrayFormatter.
Multiple Error Reporting
The Schema.filter
API also supports reporting multiple issues at once, which is useful in forms where several validation checks might fail simultaneously.
Example
ts
import {ArrayFormatter ,Schema } from "@effect/schema"import {Either } from "effect"constPassword =Schema .Trim .pipe (Schema .minLength (1))constOptionalString =Schema .optional (Schema .String )constMyForm =Schema .Struct ({password :Password ,confirm_password :Password ,name :OptionalString ,surname :OptionalString }).pipe (Schema .filter ((input ) => {constissues :Array <Schema .FilterIssue > = []// passwords must matchif (input .password !==input .confirm_password ) {issues .push ({path : ["confirm_password"],message : "Passwords do not match"})}// either name or surname must be presentif (!input .name && !input .surname ) {issues .push ({path : ["surname"],message : "Surname must be present if name is not present"})}returnissues }))console .log (JSON .stringify (Schema .decodeUnknownEither (MyForm )({password : "abc",confirm_password : "d"}).pipe (Either .mapLeft ((error ) =>ArrayFormatter .formatErrorSync (error ))),null,2))/*{"_id": "Either","_tag": "Left","left": [{"_tag": "Type","path": ["confirm_password"],"message": "Passwords do not match"},{"_tag": "Type","path": ["surname"],"message": "Surname must be present if name is not present"}]}*/
ts
import {ArrayFormatter ,Schema } from "@effect/schema"import {Either } from "effect"constPassword =Schema .Trim .pipe (Schema .minLength (1))constOptionalString =Schema .optional (Schema .String )constMyForm =Schema .Struct ({password :Password ,confirm_password :Password ,name :OptionalString ,surname :OptionalString }).pipe (Schema .filter ((input ) => {constissues :Array <Schema .FilterIssue > = []// passwords must matchif (input .password !==input .confirm_password ) {issues .push ({path : ["confirm_password"],message : "Passwords do not match"})}// either name or surname must be presentif (!input .name && !input .surname ) {issues .push ({path : ["surname"],message : "Surname must be present if name is not present"})}returnissues }))console .log (JSON .stringify (Schema .decodeUnknownEither (MyForm )({password : "abc",confirm_password : "d"}).pipe (Either .mapLeft ((error ) =>ArrayFormatter .formatErrorSync (error ))),null,2))/*{"_id": "Either","_tag": "Left","left": [{"_tag": "Type","path": ["confirm_password"],"message": "Passwords do not match"},{"_tag": "Type","path": ["surname"],"message": "Surname must be present if name is not present"}]}*/
The use of ArrayFormatter
translates the error details into a more
comprehensible format. For further details, see
ArrayFormatter.
Exposed Values
You can access the base schema for which the filter has been defined:
ts
import {Schema } from "@effect/schema"constLongString =Schema .String .pipe (Schema .filter ((s ) =>s .length >= 10))// const From: typeof Schema.StringconstFrom =LongString .from
ts
import {Schema } from "@effect/schema"constLongString =Schema .String .pipe (Schema .filter ((s ) =>s .length >= 10))// const From: typeof Schema.StringconstFrom =LongString .from
In this example, you're able to access the original schema (Schema.String
) for which the filter (LongString
) has been defined. The from
property provides access to this base schema.
String Filters
ts
import {Schema } from "@effect/schema"// Specifies maximum length of a stringSchema .String .pipe (Schema .maxLength (5))// Specifies minimum length of a stringSchema .String .pipe (Schema .minLength (5))// Equivalent to ensuring the string has a minimum length of 1Schema .NonEmptyString // Specifies exact length of a stringSchema .String .pipe (Schema .length (5))// Specifies a range for the length of a stringSchema .String .pipe (Schema .length ({min : 2,max : 4 }))// Matches a string against a regular expression patternSchema .String .pipe (Schema .pattern (/^[a-z]+$/))// Ensures a string starts with a specific substringSchema .String .pipe (Schema .startsWith ("prefix"))// Ensures a string ends with a specific substringSchema .String .pipe (Schema .endsWith ("suffix"))// Checks if a string includes a specific substringSchema .String .pipe (Schema .includes ("substring"))// Validates that a string has no leading or trailing whitespacesSchema .String .pipe (Schema .trimmed ())// Validates that a string is entirely in lowercaseSchema .String .pipe (Schema .lowercased ())
ts
import {Schema } from "@effect/schema"// Specifies maximum length of a stringSchema .String .pipe (Schema .maxLength (5))// Specifies minimum length of a stringSchema .String .pipe (Schema .minLength (5))// Equivalent to ensuring the string has a minimum length of 1Schema .NonEmptyString // Specifies exact length of a stringSchema .String .pipe (Schema .length (5))// Specifies a range for the length of a stringSchema .String .pipe (Schema .length ({min : 2,max : 4 }))// Matches a string against a regular expression patternSchema .String .pipe (Schema .pattern (/^[a-z]+$/))// Ensures a string starts with a specific substringSchema .String .pipe (Schema .startsWith ("prefix"))// Ensures a string ends with a specific substringSchema .String .pipe (Schema .endsWith ("suffix"))// Checks if a string includes a specific substringSchema .String .pipe (Schema .includes ("substring"))// Validates that a string has no leading or trailing whitespacesSchema .String .pipe (Schema .trimmed ())// Validates that a string is entirely in lowercaseSchema .String .pipe (Schema .lowercased ())
The trimmed
combinator does not make any transformations, it only
validates. If what you were looking for was a combinator to trim strings,
then check out the trim
combinator or the Trim
schema.
Number Filters
ts
import {Schema } from "@effect/schema"// Specifies a number greater than 5Schema .Number .pipe (Schema .greaterThan (5))// Specifies a number greater than or equal to 5Schema .Number .pipe (Schema .greaterThanOrEqualTo (5))// Specifies a number less than 5Schema .Number .pipe (Schema .lessThan (5))// Specifies a number less than or equal to 5Schema .Number .pipe (Schema .lessThanOrEqualTo (5))// Specifies a number between -2 and 2, inclusiveSchema .Number .pipe (Schema .between (-2, 2))// Specifies that the value must be an integerSchema .Number .pipe (Schema .int ())// Ensures the value is not NaNSchema .Number .pipe (Schema .nonNaN ())// Ensures the value is finite and not Infinity or -InfinitySchema .Number .pipe (Schema .finite ())// Specifies a positive number (> 0)Schema .Number .pipe (Schema .positive ())// Specifies a non-negative number (>= 0)Schema .Number .pipe (Schema .nonNegative ())// Specifies a negative number (< 0)Schema .Number .pipe (Schema .negative ())// Specifies a non-positive number (<= 0)Schema .Number .pipe (Schema .nonPositive ())// Specifies a number that is evenly divisible by 5Schema .Number .pipe (Schema .multipleOf (5))
ts
import {Schema } from "@effect/schema"// Specifies a number greater than 5Schema .Number .pipe (Schema .greaterThan (5))// Specifies a number greater than or equal to 5Schema .Number .pipe (Schema .greaterThanOrEqualTo (5))// Specifies a number less than 5Schema .Number .pipe (Schema .lessThan (5))// Specifies a number less than or equal to 5Schema .Number .pipe (Schema .lessThanOrEqualTo (5))// Specifies a number between -2 and 2, inclusiveSchema .Number .pipe (Schema .between (-2, 2))// Specifies that the value must be an integerSchema .Number .pipe (Schema .int ())// Ensures the value is not NaNSchema .Number .pipe (Schema .nonNaN ())// Ensures the value is finite and not Infinity or -InfinitySchema .Number .pipe (Schema .finite ())// Specifies a positive number (> 0)Schema .Number .pipe (Schema .positive ())// Specifies a non-negative number (>= 0)Schema .Number .pipe (Schema .nonNegative ())// Specifies a negative number (< 0)Schema .Number .pipe (Schema .negative ())// Specifies a non-positive number (<= 0)Schema .Number .pipe (Schema .nonPositive ())// Specifies a number that is evenly divisible by 5Schema .Number .pipe (Schema .multipleOf (5))
BigInt Filters
ts
import {Schema } from "@effect/schema"// Specifies a BigInt greater than 5Schema .BigInt .pipe (Schema .greaterThanBigInt (5n))// Specifies a BigInt greater than or equal to 5Schema .BigInt .pipe (Schema .greaterThanOrEqualToBigInt (5n))// Specifies a BigInt less than 5Schema .BigInt .pipe (Schema .lessThanBigInt (5n))// Specifies a BigInt less than or equal to 5Schema .BigInt .pipe (Schema .lessThanOrEqualToBigInt (5n))// Specifies a BigInt between -2n and 2n, inclusiveSchema .BigInt .pipe (Schema .betweenBigInt (-2n, 2n))// Specifies a positive BigInt (> 0n)Schema .BigInt .pipe (Schema .positiveBigInt ())// Specifies a non-negative BigInt (>= 0n)Schema .BigInt .pipe (Schema .nonNegativeBigInt ())// Specifies a negative BigInt (< 0n)Schema .BigInt .pipe (Schema .negativeBigInt ())// Specifies a non-positive BigInt (<= 0n)Schema .BigInt .pipe (Schema .nonPositiveBigInt ())
ts
import {Schema } from "@effect/schema"// Specifies a BigInt greater than 5Schema .BigInt .pipe (Schema .greaterThanBigInt (5n))// Specifies a BigInt greater than or equal to 5Schema .BigInt .pipe (Schema .greaterThanOrEqualToBigInt (5n))// Specifies a BigInt less than 5Schema .BigInt .pipe (Schema .lessThanBigInt (5n))// Specifies a BigInt less than or equal to 5Schema .BigInt .pipe (Schema .lessThanOrEqualToBigInt (5n))// Specifies a BigInt between -2n and 2n, inclusiveSchema .BigInt .pipe (Schema .betweenBigInt (-2n, 2n))// Specifies a positive BigInt (> 0n)Schema .BigInt .pipe (Schema .positiveBigInt ())// Specifies a non-negative BigInt (>= 0n)Schema .BigInt .pipe (Schema .nonNegativeBigInt ())// Specifies a negative BigInt (< 0n)Schema .BigInt .pipe (Schema .negativeBigInt ())// Specifies a non-positive BigInt (<= 0n)Schema .BigInt .pipe (Schema .nonPositiveBigInt ())
BigDecimal Filters
ts
import {Schema } from "@effect/schema"import {BigDecimal } from "effect"// Specifies a BigDecimal greater than 5Schema .BigDecimal .pipe (Schema .greaterThanBigDecimal (BigDecimal .fromNumber (5)))// Specifies a BigDecimal greater than or equal to 5Schema .BigDecimal .pipe (Schema .greaterThanOrEqualToBigDecimal (BigDecimal .fromNumber (5)))// Specifies a BigDecimal less than 5Schema .BigDecimal .pipe (Schema .lessThanBigDecimal (BigDecimal .fromNumber (5)))// Specifies a BigDecimal less than or equal to 5Schema .BigDecimal .pipe (Schema .lessThanOrEqualToBigDecimal (BigDecimal .fromNumber (5)))// Specifies a BigDecimal between -2 and 2, inclusiveSchema .BigDecimal .pipe (Schema .betweenBigDecimal (BigDecimal .fromNumber (-2),BigDecimal .fromNumber (2)))// Specifies a positive BigDecimal (> 0)Schema .BigDecimal .pipe (Schema .positiveBigDecimal ())// Specifies a non-negative BigDecimal (>= 0)Schema .BigDecimal .pipe (Schema .nonNegativeBigDecimal ())// Specifies a negative BigDecimal (< 0)Schema .BigDecimal .pipe (Schema .negativeBigDecimal ())// Specifies a non-positive BigDecimal (<= 0)Schema .BigDecimal .pipe (Schema .nonPositiveBigDecimal ())
ts
import {Schema } from "@effect/schema"import {BigDecimal } from "effect"// Specifies a BigDecimal greater than 5Schema .BigDecimal .pipe (Schema .greaterThanBigDecimal (BigDecimal .fromNumber (5)))// Specifies a BigDecimal greater than or equal to 5Schema .BigDecimal .pipe (Schema .greaterThanOrEqualToBigDecimal (BigDecimal .fromNumber (5)))// Specifies a BigDecimal less than 5Schema .BigDecimal .pipe (Schema .lessThanBigDecimal (BigDecimal .fromNumber (5)))// Specifies a BigDecimal less than or equal to 5Schema .BigDecimal .pipe (Schema .lessThanOrEqualToBigDecimal (BigDecimal .fromNumber (5)))// Specifies a BigDecimal between -2 and 2, inclusiveSchema .BigDecimal .pipe (Schema .betweenBigDecimal (BigDecimal .fromNumber (-2),BigDecimal .fromNumber (2)))// Specifies a positive BigDecimal (> 0)Schema .BigDecimal .pipe (Schema .positiveBigDecimal ())// Specifies a non-negative BigDecimal (>= 0)Schema .BigDecimal .pipe (Schema .nonNegativeBigDecimal ())// Specifies a negative BigDecimal (< 0)Schema .BigDecimal .pipe (Schema .negativeBigDecimal ())// Specifies a non-positive BigDecimal (<= 0)Schema .BigDecimal .pipe (Schema .nonPositiveBigDecimal ())
Duration Filters
ts
import {Schema } from "@effect/schema"// Specifies a duration greater than 5 secondsSchema .Duration .pipe (Schema .greaterThanDuration ("5 seconds"))// Specifies a duration greater than or equal to 5 secondsSchema .Duration .pipe (Schema .greaterThanOrEqualToDuration ("5 seconds"))// Specifies a duration less than 5 secondsSchema .Duration .pipe (Schema .lessThanDuration ("5 seconds"))// Specifies a duration less than or equal to 5 secondsSchema .Duration .pipe (Schema .lessThanOrEqualToDuration ("5 seconds"))// Specifies a duration between 5 seconds and 10 seconds, inclusiveSchema .Duration .pipe (Schema .betweenDuration ("5 seconds", "10 seconds"))
ts
import {Schema } from "@effect/schema"// Specifies a duration greater than 5 secondsSchema .Duration .pipe (Schema .greaterThanDuration ("5 seconds"))// Specifies a duration greater than or equal to 5 secondsSchema .Duration .pipe (Schema .greaterThanOrEqualToDuration ("5 seconds"))// Specifies a duration less than 5 secondsSchema .Duration .pipe (Schema .lessThanDuration ("5 seconds"))// Specifies a duration less than or equal to 5 secondsSchema .Duration .pipe (Schema .lessThanOrEqualToDuration ("5 seconds"))// Specifies a duration between 5 seconds and 10 seconds, inclusiveSchema .Duration .pipe (Schema .betweenDuration ("5 seconds", "10 seconds"))
Array Filters
ts
import {Schema } from "@effect/schema"// Specifies the maximum number of items in the arraySchema .Array (Schema .Number ).pipe (Schema .maxItems (2))// Specifies the minimum number of items in the arraySchema .Array (Schema .Number ).pipe (Schema .minItems (2))// Specifies the exact number of items in the arraySchema .Array (Schema .Number ).pipe (Schema .itemsCount (2))
ts
import {Schema } from "@effect/schema"// Specifies the maximum number of items in the arraySchema .Array (Schema .Number ).pipe (Schema .maxItems (2))// Specifies the minimum number of items in the arraySchema .Array (Schema .Number ).pipe (Schema .minItems (2))// Specifies the exact number of items in the arraySchema .Array (Schema .Number ).pipe (Schema .itemsCount (2))
Branded types
TypeScript's type system is structural, which means that any two types that are structurally equivalent are considered the same. This can cause issues when types that are semantically different are treated as if they were the same.
ts
typeUserId = stringtypeUsername = stringdeclare constgetUser : (id :UserId ) => objectconstmyUsername :Username = "gcanti"getUser (myUsername ) // This erroneously works
ts
typeUserId = stringtypeUsername = stringdeclare constgetUser : (id :UserId ) => objectconstmyUsername :Username = "gcanti"getUser (myUsername ) // This erroneously works
In the above example, UserId
and Username
are both aliases for the same type, string
. This means that the getUser
function can mistakenly accept a Username
as a valid UserId
, causing bugs and errors.
To avoid these kinds of issues, the Effect ecosystem provides a way to create custom types with a unique identifier attached to them. These are known as branded types.
ts
import {Brand } from "effect"typeUserId = string &Brand .Brand <"UserId">typeUsername = stringdeclare constgetUser : (id :UserId ) => objectconstmyUsername :Username = "gcanti"Argument of type 'string' is not assignable to parameter of type 'UserId'. Type 'string' is not assignable to type 'Brand<"UserId">'.2345Argument of type 'string' is not assignable to parameter of type 'UserId'. Type 'string' is not assignable to type 'Brand<"UserId">'.getUser () myUsername
ts
import {Brand } from "effect"typeUserId = string &Brand .Brand <"UserId">typeUsername = stringdeclare constgetUser : (id :UserId ) => objectconstmyUsername :Username = "gcanti"Argument of type 'string' is not assignable to parameter of type 'UserId'. Type 'string' is not assignable to type 'Brand<"UserId">'.2345Argument of type 'string' is not assignable to parameter of type 'UserId'. Type 'string' is not assignable to type 'Brand<"UserId">'.getUser () myUsername
By defining UserId
as a branded type, the getUser
function can accept only values of type UserId
, and not plain strings or other types that are compatible with strings. This helps to prevent bugs caused by accidentally passing the wrong type of value to the function.
There are two ways to define a schema for a branded type, depending on whether you:
- want to define the schema from scratch
- have already defined a branded type via
effect/Brand
and want to reuse it to define a schema
Defining a brand schema from scratch
To define a schema for a branded type from scratch, you can use the Schema.brand
function.
ts
import {Schema } from "@effect/schema"constUserId =Schema .String .pipe (Schema .brand ("UserId"))// string & Brand<"UserId">typeUserId =Schema .Schema .Type <typeofUserId >
ts
import {Schema } from "@effect/schema"constUserId =Schema .String .pipe (Schema .brand ("UserId"))// string & Brand<"UserId">typeUserId =Schema .Schema .Type <typeofUserId >
Note that you can use unique symbol
s as brands to ensure uniqueness across modules / packages:
ts
import {Schema } from "@effect/schema"constUserIdBrand =Symbol .for ("UserId")constUserId =Schema .String .pipe (Schema .brand (UserIdBrand ))// string & Brand<typeof UserIdBrand>typeUserId =Schema .Schema .Type <typeofUserId >
ts
import {Schema } from "@effect/schema"constUserIdBrand =Symbol .for ("UserId")constUserId =Schema .String .pipe (Schema .brand (UserIdBrand ))// string & Brand<typeof UserIdBrand>typeUserId =Schema .Schema .Type <typeofUserId >
Reusing an existing branded constructor
If you have already defined a branded type using the effect/Brand
module, you can reuse it to define a schema using the fromBrand
combinator exported by the @effect/schema/Schema
module.
ts
import {Schema } from "@effect/schema"import {Brand } from "effect"// the existing branded typetypeUserId = string &Brand .Brand <"UserId">constUserId =Brand .nominal <UserId >()// Define a schema for the branded typeconstUserIdSchema =Schema .String .pipe (Schema .fromBrand (UserId ))
ts
import {Schema } from "@effect/schema"import {Brand } from "effect"// the existing branded typetypeUserId = string &Brand .Brand <"UserId">constUserId =Brand .nominal <UserId >()// Define a schema for the branded typeconstUserIdSchema =Schema .String .pipe (Schema .fromBrand (UserId ))
Utilizing Default Constructors
The Schema.brand
function includes a default constructor to facilitate the creation of branded values.
ts
import {Schema } from "@effect/schema"constUserId =Schema .String .pipe (Schema .brand ("UserId"))constuserId =UserId .make ("123") // Creates a branded UserId
ts
import {Schema } from "@effect/schema"constUserId =Schema .String .pipe (Schema .brand ("UserId"))constuserId =UserId .make ("123") // Creates a branded UserId
Native enums
ts
import {Schema } from "@effect/schema"enumFruits {Apple ,Banana }// Schema.Enums<typeof Fruits>constschema =Schema .Enums (Fruits )
ts
import {Schema } from "@effect/schema"enumFruits {Apple ,Banana }// Schema.Enums<typeof Fruits>constschema =Schema .Enums (Fruits )
Accessing Enum Members
Enums are exposed under an enums
property of the schema:
ts
// Access the enum membersschema .enums // Returns all enum membersschema .enums .Apple // Access the Apple memberschema .enums .Banana // Access the Banana member
ts
// Access the enum membersschema .enums // Returns all enum membersschema .enums .Apple // Access the Apple memberschema .enums .Banana // Access the Banana member
Unions
The Schema module includes a built-in Union
constructor for composing "OR" types.
ts
import {Schema } from "@effect/schema"constschema =Schema .Union (Schema .String ,Schema .Number )
ts
import {Schema } from "@effect/schema"constschema =Schema .Union (Schema .String ,Schema .Number )
Union of Literals
While the following is perfectly acceptable:
ts
import {Schema } from "@effect/schema"constschema =Schema .Union (Schema .Literal ("a"),Schema .Literal ("b"),Schema .Literal ("c"))
ts
import {Schema } from "@effect/schema"constschema =Schema .Union (Schema .Literal ("a"),Schema .Literal ("b"),Schema .Literal ("c"))
It is possible to use Literal
and pass multiple literals, which is less cumbersome:
ts
import {Schema } from "@effect/schema"constschema =Schema .Literal ("a", "b", "c")
ts
import {Schema } from "@effect/schema"constschema =Schema .Literal ("a", "b", "c")
Nullables
ts
import {Schema } from "@effect/schema"// Represents a schema for a string or null valueSchema .NullOr (Schema .String )// Represents a schema for a string, null, or undefined valueSchema .NullishOr (Schema .String )// Represents a schema for a string or undefined valueSchema .UndefinedOr (Schema .String )
ts
import {Schema } from "@effect/schema"// Represents a schema for a string or null valueSchema .NullOr (Schema .String )// Represents a schema for a string, null, or undefined valueSchema .NullishOr (Schema .String )// Represents a schema for a string or undefined valueSchema .UndefinedOr (Schema .String )
Discriminated unions
TypeScript reference: https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions
Discriminated unions in TypeScript are a way of modeling complex data structures that may take on different forms based on a specific set of conditions or properties. They allow you to define a type that represents multiple related shapes, where each shape is uniquely identified by a shared discriminant property.
In a discriminated union, each variant of the union has a common property, called the discriminant. The discriminant is a literal type, which means it can only have a finite set of possible values. Based on the value of the discriminant property, TypeScript can infer which variant of the union is currently in use.
Here is an example of a discriminated union in TypeScript:
ts
typeCircle = {readonlykind : "circle"readonlyradius : number}typeSquare = {readonlykind : "square"readonlysideLength : number}typeShape =Circle |Square
ts
typeCircle = {readonlykind : "circle"readonlyradius : number}typeSquare = {readonlykind : "square"readonlysideLength : number}typeShape =Circle |Square
This code defines a discriminated union using the Schema module:
ts
import {Schema } from "@effect/schema"constCircle =Schema .Struct ({kind :Schema .Literal ("circle"),radius :Schema .Number })constSquare =Schema .Struct ({kind :Schema .Literal ("square"),sideLength :Schema .Number })constShape =Schema .Union (Circle ,Square )
ts
import {Schema } from "@effect/schema"constCircle =Schema .Struct ({kind :Schema .Literal ("circle"),radius :Schema .Number })constSquare =Schema .Struct ({kind :Schema .Literal ("square"),sideLength :Schema .Number })constShape =Schema .Union (Circle ,Square )
The Literal
constructor is used to define the discriminant property with a specific string literal value.
Two structs are defined for Circle
and Square
, each with their own properties. These structs represent the variants of the union.
Finally, the Union
constructor is used to create a schema for the discriminated union Shape
, which is a union of Circle
and Square
.
How to transform a simple union into a discriminated union
If you're working on a TypeScript project and you've defined a simple union to represent a particular input, you may find yourself in a situation where you're not entirely happy with how it's set up.
For example, let's say you've defined a Shape
union as a combination of Circle
and Square
without any special property:
ts
import {Schema } from "@effect/schema"constCircle =Schema .Struct ({radius :Schema .Number })constSquare =Schema .Struct ({sideLength :Schema .Number })constShape =Schema .Union (Circle ,Square )
ts
import {Schema } from "@effect/schema"constCircle =Schema .Struct ({radius :Schema .Number })constSquare =Schema .Struct ({sideLength :Schema .Number })constShape =Schema .Union (Circle ,Square )
To make your code more manageable, you may want to transform the simple union into a discriminated union. This way, TypeScript will be able to automatically determine which member of the union you're working with based on the value of a specific property.
To achieve this, you can add a special property to each member of the union, which will allow TypeScript to know which type it's dealing with at runtime.
Here's how you can transform the Shape
schema into another schema that represents a discriminated union:
ts
import {Schema } from "@effect/schema"constCircle =Schema .Struct ({radius :Schema .Number })constSquare =Schema .Struct ({sideLength :Schema .Number })constDiscriminatedShape =Schema .Union (Schema .transform (Circle ,Schema .Struct ({ ...Circle .fields ,kind :Schema .Literal ("circle") }), // Add a "kind" property with the literal value "circle" to Circle{strict : true,decode : (circle ) => ({ ...circle ,kind : "circle" asconst }), // Add the discriminant property to Circleencode : ({kind :_kind , ...rest }) =>rest // Remove the discriminant property}),Schema .transform (Square ,Schema .Struct ({ ...Square .fields ,kind :Schema .Literal ("square") }), // Add a "kind" property with the literal value "square" to Square{strict : true,decode : (square ) => ({ ...square ,kind : "square" asconst }), // Add the discriminant property to Squareencode : ({kind :_kind , ...rest }) =>rest // Remove the discriminant property}))console .log (Schema .decodeUnknownSync (DiscriminatedShape )({radius : 10 }))// Output: { kind: 'circle', radius: 10 }console .log (Schema .decodeUnknownSync (DiscriminatedShape )({sideLength : 10 }))// Output: { kind: 'square', sideLength: 10 }
ts
import {Schema } from "@effect/schema"constCircle =Schema .Struct ({radius :Schema .Number })constSquare =Schema .Struct ({sideLength :Schema .Number })constDiscriminatedShape =Schema .Union (Schema .transform (Circle ,Schema .Struct ({ ...Circle .fields ,kind :Schema .Literal ("circle") }), // Add a "kind" property with the literal value "circle" to Circle{strict : true,decode : (circle ) => ({ ...circle ,kind : "circle" asconst }), // Add the discriminant property to Circleencode : ({kind :_kind , ...rest }) =>rest // Remove the discriminant property}),Schema .transform (Square ,Schema .Struct ({ ...Square .fields ,kind :Schema .Literal ("square") }), // Add a "kind" property with the literal value "square" to Square{strict : true,decode : (square ) => ({ ...square ,kind : "square" asconst }), // Add the discriminant property to Squareencode : ({kind :_kind , ...rest }) =>rest // Remove the discriminant property}))console .log (Schema .decodeUnknownSync (DiscriminatedShape )({radius : 10 }))// Output: { kind: 'circle', radius: 10 }console .log (Schema .decodeUnknownSync (DiscriminatedShape )({sideLength : 10 }))// Output: { kind: 'square', sideLength: 10 }
The previous solution works perfectly and shows how we can add properties to our schema at will, making it easier to consume the result within our domain model.
However, it requires a lot of boilerplate. Fortunately, there is an API called Schema.attachPropertySignature
designed specifically for this use case, which allows us to achieve the same result with much less effort:
ts
import {Schema } from "@effect/schema"constCircle =Schema .Struct ({radius :Schema .Number })constSquare =Schema .Struct ({sideLength :Schema .Number })constDiscriminatedShape =Schema .Union (Circle .pipe (Schema .attachPropertySignature ("kind", "circle")),Square .pipe (Schema .attachPropertySignature ("kind", "square")))// decodingconsole .log (Schema .decodeUnknownSync (DiscriminatedShape )({radius : 10 }))// Output: { kind: 'circle', radius: 10 }// encodingconsole .log (Schema .encodeSync (DiscriminatedShape )({kind : "circle",radius : 10}))// Output: { radius: 10 }
ts
import {Schema } from "@effect/schema"constCircle =Schema .Struct ({radius :Schema .Number })constSquare =Schema .Struct ({sideLength :Schema .Number })constDiscriminatedShape =Schema .Union (Circle .pipe (Schema .attachPropertySignature ("kind", "circle")),Square .pipe (Schema .attachPropertySignature ("kind", "square")))// decodingconsole .log (Schema .decodeUnknownSync (DiscriminatedShape )({radius : 10 }))// Output: { kind: 'circle', radius: 10 }// encodingconsole .log (Schema .encodeSync (DiscriminatedShape )({kind : "circle",radius : 10}))// Output: { radius: 10 }
Please note that with Schema.attachPropertySignature
, you can only add a
property, it cannot override an existing one.
Exposed Values
You can access the individual members of a union schema represented as a tuple:
ts
import {Schema } from "@effect/schema"constschema =Schema .Union (Schema .String ,Schema .Number )// Accesses the members of the unionconstmembers =schema .members
ts
import {Schema } from "@effect/schema"constschema =Schema .Union (Schema .String ,Schema .Number )// Accesses the members of the unionconstmembers =schema .members
Tuples
Required Elements
To define a tuple with required elements, you specify the list of elements:
ts
import {Schema } from "@effect/schema"constschema =Schema .Tuple (Schema .String ,Schema .Number )
ts
import {Schema } from "@effect/schema"constschema =Schema .Tuple (Schema .String ,Schema .Number )
Append a Required Element
ts
import {Schema } from "@effect/schema"consttuple1 =Schema .Tuple (Schema .String ,Schema .Number )consttuple2 =Schema .Tuple (...tuple1 .elements ,Schema .Boolean )
ts
import {Schema } from "@effect/schema"consttuple1 =Schema .Tuple (Schema .String ,Schema .Number )consttuple2 =Schema .Tuple (...tuple1 .elements ,Schema .Boolean )
Optional Elements
To define an optional element, wrap the schema of the element with the optionalElement
constructor:
ts
import {Schema } from "@effect/schema"constschema =Schema .Tuple (Schema .String , // required elementSchema .optionalElement (Schema .Number ) // optional element)
ts
import {Schema } from "@effect/schema"constschema =Schema .Tuple (Schema .String , // required elementSchema .optionalElement (Schema .Number ) // optional element)
Rest Element
To define rest elements, follow the list of elements (required or optional) with an element for the rest:
ts
import {Schema } from "@effect/schema"constschema =Schema .Tuple ([Schema .String ,Schema .optionalElement (Schema .Number )], // elementsSchema .Boolean // rest)
ts
import {Schema } from "@effect/schema"constschema =Schema .Tuple ([Schema .String ,Schema .optionalElement (Schema .Number )], // elementsSchema .Boolean // rest)
Optionally, you can include other elements after the rest:
ts
import {Schema } from "@effect/schema"constschema =Schema .Tuple ([Schema .String ,Schema .optionalElement (Schema .Number )], // elementsSchema .Boolean , // restSchema .String // additional element)
ts
import {Schema } from "@effect/schema"constschema =Schema .Tuple ([Schema .String ,Schema .optionalElement (Schema .Number )], // elementsSchema .Boolean , // restSchema .String // additional element)
Exposed Values
You can access the elements and rest elements of a tuple schema:
ts
import {Schema } from "@effect/schema"constschema =Schema .Tuple ([Schema .String ,Schema .optionalElement (Schema .Number )], // elementsSchema .Boolean , // restSchema .String // additional element)// Accesses the elements of the tupleconsttupleElements =schema .elements // Accesses the rest elements of the tupleconstrestElements =schema .rest
ts
import {Schema } from "@effect/schema"constschema =Schema .Tuple ([Schema .String ,Schema .optionalElement (Schema .Number )], // elementsSchema .Boolean , // restSchema .String // additional element)// Accesses the elements of the tupleconsttupleElements =schema .elements // Accesses the rest elements of the tupleconstrestElements =schema .rest
Annotations
Annotations are used to add metadata to tuple elements, which can describe the purpose or requirements of each element more clearly. This can be particularly useful when generating documentation or JSON schemas from your schemas.
ts
import {JSONSchema ,Schema } from "@effect/schema"// Defining a tuple with annotations for each coordinate in a pointconstPoint =Schema .Tuple (Schema .element (Schema .Number ).annotations ({title : "X",description : "X coordinate"}),Schema .optionalElement (Schema .Number ).annotations ({title : "Y",description : "optional Y coordinate"}))// Generating a JSON Schema from the tupleconsole .log (JSONSchema .make (Point ))/*Output:{'$schema': 'http://json-schema.org/draft-07/schema#',type: 'array',minItems: 1,items: [{ type: 'number', description: 'X coordinate', title: 'X' },{type: 'number',description: 'optional Y coordinate',title: 'Y'}],additionalItems: false}*/
ts
import {JSONSchema ,Schema } from "@effect/schema"// Defining a tuple with annotations for each coordinate in a pointconstPoint =Schema .Tuple (Schema .element (Schema .Number ).annotations ({title : "X",description : "X coordinate"}),Schema .optionalElement (Schema .Number ).annotations ({title : "Y",description : "optional Y coordinate"}))// Generating a JSON Schema from the tupleconsole .log (JSONSchema .make (Point ))/*Output:{'$schema': 'http://json-schema.org/draft-07/schema#',type: 'array',minItems: 1,items: [{ type: 'number', description: 'X coordinate', title: 'X' },{type: 'number',description: 'optional Y coordinate',title: 'Y'}],additionalItems: false}*/
Arrays
ts
import {Schema } from "@effect/schema"constschema =Schema .Array (Schema .Number )
ts
import {Schema } from "@effect/schema"constschema =Schema .Array (Schema .Number )
Exposed Values
You can access the value of an array schema:
ts
import {Schema } from "@effect/schema"constschema =Schema .Array (Schema .String )// Accesses the valueconstvalue =schema .value // typeof Schema.String
ts
import {Schema } from "@effect/schema"constschema =Schema .Array (Schema .String )// Accesses the valueconstvalue =schema .value // typeof Schema.String
Mutable Arrays
By default, when you use Schema.Array
, it generates a type marked as readonly. The mutable
combinator is a useful function for creating a new schema with a mutable type in a shallow manner:
ts
import {Schema } from "@effect/schema"constschema =Schema .mutable (Schema .Array (Schema .Number ))
ts
import {Schema } from "@effect/schema"constschema =Schema .mutable (Schema .Array (Schema .Number ))
Non empty arrays
ts
import {Schema } from "@effect/schema"constschema =Schema .NonEmptyArray (Schema .Number )
ts
import {Schema } from "@effect/schema"constschema =Schema .NonEmptyArray (Schema .Number )
Exposed Values
You can access the value of a non-empty array schema:
ts
import {Schema } from "@effect/schema"constschema =Schema .NonEmptyArray (Schema .String )// Accesses the valueconstvalue =schema .value
ts
import {Schema } from "@effect/schema"constschema =Schema .NonEmptyArray (Schema .String )// Accesses the valueconstvalue =schema .value
Records
String Keyed Records
ts
import {Schema } from "@effect/schema"constopaque =Schema .Record ({key :Schema .String ,value :Schema .Number })// Schema<{ readonly [x: string]: number; }>constschema =Schema .asSchema (opaque )
ts
import {Schema } from "@effect/schema"constopaque =Schema .Record ({key :Schema .String ,value :Schema .Number })// Schema<{ readonly [x: string]: number; }>constschema =Schema .asSchema (opaque )
Union of Literals as Keys
ts
import {Schema } from "@effect/schema"constopaque =Schema .Record ({key :Schema .Union (Schema .Literal ("a"),Schema .Literal ("b")),value :Schema .Number })// Schema<{ readonly a: number; readonly b: number; }>constschema =Schema .asSchema (opaque )
ts
import {Schema } from "@effect/schema"constopaque =Schema .Record ({key :Schema .Union (Schema .Literal ("a"),Schema .Literal ("b")),value :Schema .Number })// Schema<{ readonly a: number; readonly b: number; }>constschema =Schema .asSchema (opaque )
Applying Key Refinements
ts
import {Schema } from "@effect/schema"constschema =Schema .Record ({key :Schema .String .pipe (Schema .minLength (2)),value :Schema .Number })
ts
import {Schema } from "@effect/schema"constschema =Schema .Record ({key :Schema .String .pipe (Schema .minLength (2)),value :Schema .Number })
Symbol Keyed Records
ts
import {Schema } from "@effect/schema"constschema =Schema .Record ({key :Schema .SymbolFromSelf ,value :Schema .Number })
ts
import {Schema } from "@effect/schema"constschema =Schema .Record ({key :Schema .SymbolFromSelf ,value :Schema .Number })
Employing Template Literal Keys
ts
import {Schema } from "@effect/schema"constopaque =Schema .Record ({key :Schema .TemplateLiteral (Schema .Literal ("a"),Schema .String ),value :Schema .Number })// Schema<{ readonly [x: `a${string}`]: number; }>constschema =Schema .asSchema (opaque )
ts
import {Schema } from "@effect/schema"constopaque =Schema .Record ({key :Schema .TemplateLiteral (Schema .Literal ("a"),Schema .String ),value :Schema .Number })// Schema<{ readonly [x: `a${string}`]: number; }>constschema =Schema .asSchema (opaque )
Creating Mutable Records
By default, when you use Schema.Record
, it generates a type marked as readonly. The mutable
combinator is a useful function for creating a new schema with a mutable type in a shallow manner:
ts
import {Schema } from "@effect/schema"constschema =Schema .mutable (Schema .Record ({key :Schema .String ,value :Schema .Number }))
ts
import {Schema } from "@effect/schema"constschema =Schema .mutable (Schema .Record ({key :Schema .String ,value :Schema .Number }))
Exposed Values
You can access the key and the value of a record schema:
ts
import {Schema } from "@effect/schema"constschema =Schema .Record ({key :Schema .String ,value :Schema .Number })// Accesses the keyconstkey =schema .key // Accesses the valueconstvalue =schema .value
ts
import {Schema } from "@effect/schema"constschema =Schema .Record ({key :Schema .String ,value :Schema .Number })// Accesses the keyconstkey =schema .key // Accesses the valueconstvalue =schema .value
Structs
Structs are used to define schemas for objects with specific properties. Here's how you can create and use a struct schema:
ts
import {Schema } from "@effect/schema"// Define a struct schema for an object with properties "name" (string) and "age" (number)constMyStruct =Schema .Struct ({name :Schema .String ,age :Schema .Number })
ts
import {Schema } from "@effect/schema"// Define a struct schema for an object with properties "name" (string) and "age" (number)constMyStruct =Schema .Struct ({name :Schema .String ,age :Schema .Number })
The MyStruct
constant will have the type
ts
const MyStruct: Schema.Struct<{name: typeof Schema.Stringage: typeof Schema.Number}>
ts
const MyStruct: Schema.Struct<{name: typeof Schema.Stringage: typeof Schema.Number}>
representing the structure of the object.
To view the detailed type of MyStruct
, you can use the Schema.asSchema
function:
ts
/*const schema: Schema.Schema<{readonly name: string;readonly age: number;}, {readonly name: string;readonly age: number;}, never>*/constschema =Schema .asSchema (MyStruct )
ts
/*const schema: Schema.Schema<{readonly name: string;readonly age: number;}, {readonly name: string;readonly age: number;}, never>*/constschema =Schema .asSchema (MyStruct )
Note that Schema.Struct({})
models the TypeScript type {}
, which is
similar to unknown
. This means that the schema will allow any type of data
to pass through without validation.
Index Signatures
The Struct
constructor optionally accepts a list of key/value pairs representing index signatures:
ts
(props, ...indexSignatures) => Struct<...>
ts
(props, ...indexSignatures) => Struct<...>
Example
ts
import {Schema } from "@effect/schema"/*Schema.TypeLiteral<{a: typeof Schema.Number;}, readonly [{readonly key: typeof Schema.String;readonly value: typeof Schema.Number;}]>*/constopaque =Schema .Struct ({a :Schema .Number },{key :Schema .String ,value :Schema .Number })/*Schema.Schema<{readonly [x: string]: number;readonly a: number;}, {readonly [x: string]: number;readonly a: number;}, never>*/constnonOpaque =Schema .asSchema (opaque )
ts
import {Schema } from "@effect/schema"/*Schema.TypeLiteral<{a: typeof Schema.Number;}, readonly [{readonly key: typeof Schema.String;readonly value: typeof Schema.Number;}]>*/constopaque =Schema .Struct ({a :Schema .Number },{key :Schema .String ,value :Schema .Number })/*Schema.Schema<{readonly [x: string]: number;readonly a: number;}, {readonly [x: string]: number;readonly a: number;}, never>*/constnonOpaque =Schema .asSchema (opaque )
Since the Schema.Record
constructor returns a schema that exposes both the key
and the value
, instead of passing a bare object { key, value }
, you can use the Record
constructor:
ts
import {Schema } from "@effect/schema"/*Schema.TypeLiteral<{a: typeof Schema.Number;}, readonly [Schema.Record$<typeof Schema.String, typeof Schema.Number>]>*/constopaque =Schema .Struct ({a :Schema .Number },Schema .Record ({key :Schema .String ,value :Schema .Number }))/*Schema.Schema<{readonly [x: string]: number;readonly a: number;}, {readonly [x: string]: number;readonly a: number;}, never>*/constnonOpaque =Schema .asSchema (opaque )
ts
import {Schema } from "@effect/schema"/*Schema.TypeLiteral<{a: typeof Schema.Number;}, readonly [Schema.Record$<typeof Schema.String, typeof Schema.Number>]>*/constopaque =Schema .Struct ({a :Schema .Number },Schema .Record ({key :Schema .String ,value :Schema .Number }))/*Schema.Schema<{readonly [x: string]: number;readonly a: number;}, {readonly [x: string]: number;readonly a: number;}, never>*/constnonOpaque =Schema .asSchema (opaque )
Exposed Values
You can access the fields and the records of a struct schema:
ts
import {Schema } from "@effect/schema"constschema =Schema .Struct ({a :Schema .Number },Schema .Record ({key :Schema .String ,value :Schema .Number }))// Accesses the fieldsconstfields =schema .fields // Accesses the recordsconstrecords =schema .records
ts
import {Schema } from "@effect/schema"constschema =Schema .Struct ({a :Schema .Number },Schema .Record ({key :Schema .String ,value :Schema .Number }))// Accesses the fieldsconstfields =schema .fields // Accesses the recordsconstrecords =schema .records
Mutable Properties
By default, when you use Schema.Struct
, it generates a type with properties that are marked as readonly.
The Schema.mutable
combinator is a useful function for creating a new schema with properties made mutable in a shallow manner:
ts
import {Schema } from "@effect/schema"constopaque =Schema .mutable (Schema .Struct ({a :Schema .String ,b :Schema .Number }))/*Schema.Schema<{a: string;b: number;}, {a: string;b: number;}, never>*/constnonOpaque =Schema .asSchema (opaque )
ts
import {Schema } from "@effect/schema"constopaque =Schema .mutable (Schema .Struct ({a :Schema .String ,b :Schema .Number }))/*Schema.Schema<{a: string;b: number;}, {a: string;b: number;}, never>*/constnonOpaque =Schema .asSchema (opaque )
Property Signatures
Basic Usage of Property Signatures
A PropertySignature
generally represents a transformation from a "From" field:
ts
{fromKey: fromType}
ts
{fromKey: fromType}
to a "To" field:
ts
{toKey: toType}
ts
{toKey: toType}
Let's start with the simple definition of a property signature that can be used to add annotations:
ts
import {Schema } from "@effect/schema"/*Schema.Struct<{name: typeof Schema.String;age: Schema.propertySignature<typeof Schema.NumberFromString>;}>*/constPerson =Schema .Struct ({name :Schema .String ,age :Schema .propertySignature (Schema .NumberFromString ).annotations ({title : "Age"})})
ts
import {Schema } from "@effect/schema"/*Schema.Struct<{name: typeof Schema.String;age: Schema.propertySignature<typeof Schema.NumberFromString>;}>*/constPerson =Schema .Struct ({name :Schema .String ,age :Schema .propertySignature (Schema .NumberFromString ).annotations ({title : "Age"})})
Let's delve into the details of all the information contained in the type of a PropertySignature
:
ts
age: PropertySignature<ToToken,ToType,FromKey,FromToken,FromType,HasDefault,Context>
ts
age: PropertySignature<ToToken,ToType,FromKey,FromToken,FromType,HasDefault,Context>
Param Name | Description |
---|---|
age | Key of the "To" field |
ToToken | Indicates field requirement: "?:" for optional, ":" for required |
ToType | Type of the "To" field |
FromKey | (Optional, default = never ) Indicates the source field key, typically the same as "To" field key unless specified |
FormToken | Indicates source field requirement: "?:" for optional, ":" for required |
FromType | Type of the "From" field |
HasDefault | Indicates if there is a constructor default value (Boolean) |
In our case, the type
ts
PropertySignature<":", number, never, ":", string, false, never>
ts
PropertySignature<":", number, never, ":", string, false, never>
indicates that there is the following transformation:
Param Name | Description |
---|---|
age | Key of the "To" field |
ToToken | ":" indicates that the age field is required |
ToType | Type of the age field is number |
FromKey | never indicates that the decoding occurs from the same field named age |
FormToken | ":" indicates that the decoding occurs from a required age field |
FromType | Type of the "From" field is string |
HasDefault | false : indicates there is no default value |
Now, suppose the field from which decoding occurs is named "AGE"
, but for our model, we want to keep the name in lowercase "age"
. To achieve this result, we need to map the field key from "AGE"
to "age"
, and to do that, we can use the fromKey
combinator:
ts
import {Schema } from "@effect/schema"/*Schema.Struct<{name: typeof Schema.String;age: Schema.PropertySignature<":", number, "AGE", ":", string, false, never>;}>*/constPerson =Schema .Struct ({name :Schema .String ,age :Schema .propertySignature (Schema .NumberFromString ).pipe (Schema .fromKey ("AGE"))})
ts
import {Schema } from "@effect/schema"/*Schema.Struct<{name: typeof Schema.String;age: Schema.PropertySignature<":", number, "AGE", ":", string, false, never>;}>*/constPerson =Schema .Struct ({name :Schema .String ,age :Schema .propertySignature (Schema .NumberFromString ).pipe (Schema .fromKey ("AGE"))})
This modification is represented in the type of the created PropertySignature
:
ts
// fromKey ----------------------vPropertySignature<":", number, "AGE", ":", string, false, never>
ts
// fromKey ----------------------vPropertySignature<":", number, "AGE", ":", string, false, never>
Now, let's see an example of decoding:
ts
console .log (Schema .decodeUnknownSync (Person )({name : "name",AGE : "18" }))// Output: { name: 'name', age: 18 }
ts
console .log (Schema .decodeUnknownSync (Person )({name : "name",AGE : "18" }))// Output: { name: 'name', age: 18 }
Optional Fields
Basic Optional Property
Schema.optional(schema: Schema<A, I, R>)
defines a basic optional property that handles different inputs and outputs during decoding and encoding:
- Decoding:
<missing value>
remains<missing value>
undefined
remainsundefined
- Input
i: I
transforms toa: A
- Encoding:
<missing value>
remains<missing value>
undefined
remainsundefined
- Input
a: A
transforms back toi: I
Optional with Nullability
Schema.optionalWith(schema: Schema<A, I, R>, { nullable: true })
allows handling of null
values as equivalent to missing values:
- Decoding:
<missing value>
remains<missing value>
undefined
remainsundefined
null
transforms to<missing value>
- Input
i: I
transforms toa: A
- Encoding:
<missing value>
remains<missing value>
undefined
remainsundefined
- Input
a: A
transforms back toi: I
Optional with Exactness
Schema.optionalWith(schema: Schema<A, I, R>, { exact: true })
ensures that only the exact types specified are handled, excluding undefined
:
- Decoding:
<missing value>
remains<missing value>
- Input
i: I
transforms toa: A
- Encoding:
<missing value>
remains<missing value>
- Input
a: A
transforms back toi: I
Combining Nullability and Exactness
Schema.optionalWith(schema: Schema<A, I, R>, { exact: true, nullable: true })
combines handling for exact types and null values:
- Decoding:
<missing value>
remains<missing value>
null
transforms to<missing value>
- Input
i: I
transforms toa: A
- Encoding:
<missing value>
remains<missing value>
- Input
a: A
transforms back toi: I
Representing Optional Fields with never Type
When defining types in TypeScript that include optional fields with the type never
, such as:
ts
type MyType = {readonly foo?: never}
ts
type MyType = {readonly foo?: never}
the approach varies based on the exactOptionalPropertyTypes
configuration in your tsconfig.json
TypeScript Configuration: exactOptionalPropertyTypes = false
When this feature is turned off, you can employ the Schema.optional
function. This approach allows the field to implicitly accept undefined
as a value.
ts
import {Schema } from "@effect/schema"constschema =Schema .Struct ({foo :Schema .optional (Schema .Never )})
ts
import {Schema } from "@effect/schema"constschema =Schema .Struct ({foo :Schema .optional (Schema .Never )})
TypeScript Configuration: exactOptionalPropertyTypes = true
When this feature is turned on, the Schema.optionalWith
function is recommended.
It ensures stricter enforcement of the field's absence.
ts
import {Schema } from "@effect/schema"constschema =Schema .Struct ({foo :Schema .optionalWith (Schema .Never , {exact : true })})
ts
import {Schema } from "@effect/schema"constschema =Schema .Struct ({foo :Schema .optionalWith (Schema .Never , {exact : true })})
Default Values
The default
option in schemas allows you to set default values that are applied during both decoding and object construction phases. This feature ensures that even if certain properties are not provided by the user, the system will automatically use the specified default values.
Example
Let's see how default values work in both the decoding and constructing phases, illustrating how the default value is applied when certain properties are not provided.
ts
import {Schema } from "@effect/schema"constProduct =Schema .Struct ({name :Schema .String ,price :Schema .NumberFromString ,quantity :Schema .optionalWith (Schema .NumberFromString , {default : () => 1 })})// Applying defaults in the decoding phaseconsole .log (Schema .decodeUnknownSync (Product )({name : "Laptop",price : "999" }))// Output: { name: 'Laptop', price: 999, quantity: 1 }console .log (Schema .decodeUnknownSync (Product )({name : "Laptop",price : "999",quantity : "2"}))// Output: { name: 'Laptop', price: 999, quantity: 2 }// Applying defaults in the constructorconsole .log (Product .make ({name : "Laptop",price : 999 }))// Output: { name: 'Laptop', price: 999, quantity: 1 }console .log (Product .make ({name : "Laptop",price : 999,quantity : 2 }))// Output: { name: 'Laptop', price: 999, quantity: 2 }
ts
import {Schema } from "@effect/schema"constProduct =Schema .Struct ({name :Schema .String ,price :Schema .NumberFromString ,quantity :Schema .optionalWith (Schema .NumberFromString , {default : () => 1 })})// Applying defaults in the decoding phaseconsole .log (Schema .decodeUnknownSync (Product )({name : "Laptop",price : "999" }))// Output: { name: 'Laptop', price: 999, quantity: 1 }console .log (Schema .decodeUnknownSync (Product )({name : "Laptop",price : "999",quantity : "2"}))// Output: { name: 'Laptop', price: 999, quantity: 2 }// Applying defaults in the constructorconsole .log (Product .make ({name : "Laptop",price : 999 }))// Output: { name: 'Laptop', price: 999, quantity: 1 }console .log (Product .make ({name : "Laptop",price : 999,quantity : 2 }))// Output: { name: 'Laptop', price: 999, quantity: 2 }
Schema.optionalWith
can be configured with additional options to handle decoding and encoding precisely:
Basic Optional with Default
Schema.optionalWith(schema: Schema<A, I, R>, { default: () => A })
- Decoding: Translates missing or undefined inputs to a default value.
- Encoding: Input
a: A
transforms back toi: I
Optional with Exactness
Schema.optionalWith(schema: Schema<A, I, R>, { exact: true, default: () => A })
- Decoding: Applies the default value only if the input is missing.
- Encoding: Input
a: A
transforms back toi: I
Optional with Nullability
Schema.optionalWith(schema: Schema<A, I, R>, { nullable: true, default: () => A })
- Decoding: Treats null, undefined, or missing inputs as defaults.
- Encoding: Input
a: A
transforms back toi: I
Combining Exactness and Nullability
Schema.optionalWith(schema: Schema<A, I, R>, { exact: true, nullable: true, default: () => A })
- Decoding: Defaults are applied when values are null or missing.
- Encoding: Input
a: A
transforms back toi: I
Optional Fields as Options
Basic Optional with Option Type
optionalWith(schema: Schema<A, I, R>, { as: "Option" })
- Decoding:
- Missing values or
undefined
are converted toOption.none()
. - Provided values (
i: I
) are converted toOption.some(a: A)
.
- Missing values or
- Encoding:
Option.none()
results in the value being omitted.Option.some(a: A)
is converted back to the original input (i: I
).
Optional with Exactness
optionalWith(schema: Schema<A, I, R>, { exact: true, as: "Option" })
- Decoding:
- Only truly missing values are converted to
Option.none()
. - Provided values (
i: I
) are converted toOption.some(a)
.
- Only truly missing values are converted to
- Encoding:
Option.none()
results in the value being omitted.Option.some(a: A)
is converted back to the original input (i: I
).
Optional with Nullability
optionalWith(schema: Schema<A, I, R>, { nullable: true, as: "Option" })
- Decoding:
- Treats missing,
undefined
, andnull
values all asOption.none()
. - Provided values (
i: I
) are converted toOption.some(a: A)
.
- Treats missing,
- Encoding:
Option.none()
results in the value being omitted.Option.some(a: A)
is converted back to the original input (i: I
).
Combining Exactness and Nullability
optionalWith(schema: Schema<A, I, R>, { exact: true, nullable: true, as: "Option" })
- Decoding:
- Missing or
null
values lead toOption.none()
. - Provided values (
i: I
) are converted toOption.some(a: A)
.
- Missing or
- Encoding:
Option.none()
results in the value being omitted.Option.some(a: A)
is converted back to the original input (i: I
).
Optional Fields Primitives
The Schema.optional
and Schema.optionalWith
functions are built on two foundational operations: Schema.optionalToOptional
and Schema.optionalToRequired
.
These functions provide nuanced control over how optional fields are handled in your schemas, allowing for precise property signatures.
optionalToOptional
The Schema.optionalToOptional
API is used to manage the transformation from an optional field to another optional field.
With this, we can control both the output type and the presence or absence of the field.
For example a common use case is to equate a specific value in the source field with the absence of value in the destination field.
Here's the signature of the optionalToOptional
API:
ts
export const optionalToOptional = <FA, FI, FR, TA, TI, TR>(from: Schema<FA, FI, FR>,to: Schema<TA, TI, TR>,options: {readonly decode: (o: Option.Option<FA>) => Option.Option<TI>,readonly encode: (o: Option.Option<TI>) => Option.Option<FA>}): PropertySignature<"?:", TA, never, "?:", FI, false, FR | TR>
ts
export const optionalToOptional = <FA, FI, FR, TA, TI, TR>(from: Schema<FA, FI, FR>,to: Schema<TA, TI, TR>,options: {readonly decode: (o: Option.Option<FA>) => Option.Option<TI>,readonly encode: (o: Option.Option<TI>) => Option.Option<FA>}): PropertySignature<"?:", TA, never, "?:", FI, false, FR | TR>
As you can see, we can transform the type by specifying a schema for to
, which can be different from the schema of from
.
Additionally, we can control the presence or absence of the field using decode
and encode
, with the following meanings:
Option.none()
as an argument means the value is missing in the inputOption.none()
as a return value means the value will be missing in the output
Example
Suppose we have an optional field of type string
, and we want to exclude empty strings from the output. In other words, if the input contains an empty string, we want the field to be absent in the output.
ts
import {Schema } from "@effect/schema"import {identity ,Option } from "effect"constschema =Schema .Struct ({a :Schema .optionalToOptional (Schema .String ,Schema .String , {decode : (input ) => {if (Option .isNone (input )) {// If the field is absent in the input, returning `Option.none()` will make it absent in the output tooreturnOption .none ()}constvalue =input .value if (value === "") {// If the field is present in the input but is an empty string, returning `Option.none()` will make it absent in the outputreturnOption .none ()}// If the field is present in the input and is not an empty string, returning `Option.some` will make it present in the outputreturnOption .some (value )},// Here in the encoding part, we can decide to handle things in the same way as in the decoding phase// or handle them differently. For example, we can leave everything unchanged and use the identity functionencode :identity })})constdecode =Schema .decodeUnknownSync (schema )console .log (decode ({})) // Output: {}console .log (decode ({a : "" })) // Output: {}console .log (decode ({a : "a non-empty string" })) // Output: { a: 'a non-empty string' }constencode =Schema .encodeSync (schema )console .log (encode ({})) // Output: {}console .log (encode ({a : "" })) // Output: { a: '' }console .log (encode ({a : "foo" })) // Output: { a: 'foo' }
ts
import {Schema } from "@effect/schema"import {identity ,Option } from "effect"constschema =Schema .Struct ({a :Schema .optionalToOptional (Schema .String ,Schema .String , {decode : (input ) => {if (Option .isNone (input )) {// If the field is absent in the input, returning `Option.none()` will make it absent in the output tooreturnOption .none ()}constvalue =input .value if (value === "") {// If the field is present in the input but is an empty string, returning `Option.none()` will make it absent in the outputreturnOption .none ()}// If the field is present in the input and is not an empty string, returning `Option.some` will make it present in the outputreturnOption .some (value )},// Here in the encoding part, we can decide to handle things in the same way as in the decoding phase// or handle them differently. For example, we can leave everything unchanged and use the identity functionencode :identity })})constdecode =Schema .decodeUnknownSync (schema )console .log (decode ({})) // Output: {}console .log (decode ({a : "" })) // Output: {}console .log (decode ({a : "a non-empty string" })) // Output: { a: 'a non-empty string' }constencode =Schema .encodeSync (schema )console .log (encode ({})) // Output: {}console .log (encode ({a : "" })) // Output: { a: '' }console .log (encode ({a : "foo" })) // Output: { a: 'foo' }
optionalToRequired
The Schema.optionalToRequired
API allows us to transform an optional field into a required one, applying custom logic if the field is absent in the input.
ts
export const optionalToRequired = <FA, FI, FR, TA, TI, TR>(from: Schema<FA, FI, FR>,to: Schema<TA, TI, TR>,options: {readonly decode: (o: Option.Option<FA>) => TI,readonly encode: (ti: TI) => Option.Option<FA>}): PropertySignature<":", TA, never, "?:", FI, false, FR | TR>
ts
export const optionalToRequired = <FA, FI, FR, TA, TI, TR>(from: Schema<FA, FI, FR>,to: Schema<TA, TI, TR>,options: {readonly decode: (o: Option.Option<FA>) => TI,readonly encode: (ti: TI) => Option.Option<FA>}): PropertySignature<":", TA, never, "?:", FI, false, FR | TR>
We can control the presence or absence of the field using decode
and encode
, with the following meanings:
Option.none()
as an argument means the value is missing in the inputOption.none()
as a return value means the value will be missing in the output
Example
For instance, a common use case is to assign a default value to the field in the output if it's missing in the input.
ts
import {Schema } from "@effect/schema"import {Option } from "effect"constschema =Schema .Struct ({a :Schema .optionalToRequired (Schema .String ,Schema .String , {decode : (input ) => {if (Option .isNone (input )) {// If the field is absent in the input, we can return the default value for the field in the outputreturn "default value"}// If the field is present in the input, return its value as it is in the outputreturninput .value },// During encoding, we can choose to handle things differently, or simply return the same value present in the input for the outputencode : (a ) =>Option .some (a )})})constdecode =Schema .decodeUnknownSync (schema )console .log (decode ({})) // Output: { a: 'default value' }console .log (decode ({a : "foo" })) // Output: { a: 'foo' }constencode =Schema .encodeSync (schema )console .log (encode ({a : "foo" })) // Output: { a: 'foo' }
ts
import {Schema } from "@effect/schema"import {Option } from "effect"constschema =Schema .Struct ({a :Schema .optionalToRequired (Schema .String ,Schema .String , {decode : (input ) => {if (Option .isNone (input )) {// If the field is absent in the input, we can return the default value for the field in the outputreturn "default value"}// If the field is present in the input, return its value as it is in the outputreturninput .value },// During encoding, we can choose to handle things differently, or simply return the same value present in the input for the outputencode : (a ) =>Option .some (a )})})constdecode =Schema .decodeUnknownSync (schema )console .log (decode ({})) // Output: { a: 'default value' }console .log (decode ({a : "foo" })) // Output: { a: 'foo' }constencode =Schema .encodeSync (schema )console .log (encode ({a : "foo" })) // Output: { a: 'foo' }
requiredToOptional
This API allows developers to specify how a field that is normally required can be treated as optional based on custom logic.
ts
export const requiredToOptional = <FA, FI, FR, TA, TI, TR>(from: Schema<FA, FI, FR>,to: Schema<TA, TI, TR>,options: {readonly decode: (fa: FA) => Option.Option<TI>readonly encode: (o: Option.Option<TI>) => FA}): PropertySignature<"?:", TA, never, ":", FI, false, FR | TR>
ts
export const requiredToOptional = <FA, FI, FR, TA, TI, TR>(from: Schema<FA, FI, FR>,to: Schema<TA, TI, TR>,options: {readonly decode: (fa: FA) => Option.Option<TI>readonly encode: (o: Option.Option<TI>) => FA}): PropertySignature<"?:", TA, never, ":", FI, false, FR | TR>
We can control the presence or absence of the field using decode
and encode
, with the following meanings:
Option.none()
as an argument means the value is missing in the inputOption.none()
as a return value means the value will be missing in the output
Example
Let's look at a practical example where a field name
that is typically required can be considered optional if it's an empty string during decoding, and ensure there is always a value during encoding by providing a default.
ts
import {Schema } from "@effect/schema"import {Option } from "effect"constschema =Schema .Struct ({name :Schema .requiredToOptional (Schema .String ,Schema .String , {decode :Option .liftPredicate ((s ) =>s !== ""), // empty string is considered as absentencode :Option .getOrElse (() => "")})})constdecode =Schema .decodeUnknownSync (schema )console .log (decode ({name : "John" })) // Output: { name: 'John' }console .log (decode ({name : "" })) // Output: {}constencode =Schema .encodeSync (schema )console .log (encode ({name : "John" })) // { name: 'John' }console .log (encode ({})) // Output: { name: '' }
ts
import {Schema } from "@effect/schema"import {Option } from "effect"constschema =Schema .Struct ({name :Schema .requiredToOptional (Schema .String ,Schema .String , {decode :Option .liftPredicate ((s ) =>s !== ""), // empty string is considered as absentencode :Option .getOrElse (() => "")})})constdecode =Schema .decodeUnknownSync (schema )console .log (decode ({name : "John" })) // Output: { name: 'John' }console .log (decode ({name : "" })) // Output: {}constencode =Schema .encodeSync (schema )console .log (encode ({name : "John" })) // { name: 'John' }console .log (encode ({})) // Output: { name: '' }
Renaming Properties
Renaming a Property During Definition
To rename a property directly during schema creation, you can utilize the Schema.fromKey
function. This function is particularly useful when you want to map properties from the input object to different names in the resulting schema object.
Example: Renaming a Required Property
ts
import {Schema } from "@effect/schema"constschema =Schema .Struct ({a :Schema .propertySignature (Schema .String ).pipe (Schema .fromKey ("c")),b :Schema .Number })console .log (Schema .decodeUnknownSync (schema )({c : "c",b : 1 }))// Output: { a: "c", b: 1 }
ts
import {Schema } from "@effect/schema"constschema =Schema .Struct ({a :Schema .propertySignature (Schema .String ).pipe (Schema .fromKey ("c")),b :Schema .Number })console .log (Schema .decodeUnknownSync (schema )({c : "c",b : 1 }))// Output: { a: "c", b: 1 }
Example: Renaming an Optional Property
ts
import {Schema } from "@effect/schema"constschema =Schema .Struct ({a :Schema .optional (Schema .String ).pipe (Schema .fromKey ("c")),b :Schema .Number })console .log (Schema .decodeUnknownSync (schema )({c : "c",b : 1 }))// Output: { b: 1, a: "c" }console .log (Schema .decodeUnknownSync (schema )({b : 1 }))// Output: { b: 1 }
ts
import {Schema } from "@effect/schema"constschema =Schema .Struct ({a :Schema .optional (Schema .String ).pipe (Schema .fromKey ("c")),b :Schema .Number })console .log (Schema .decodeUnknownSync (schema )({c : "c",b : 1 }))// Output: { b: 1, a: "c" }console .log (Schema .decodeUnknownSync (schema )({b : 1 }))// Output: { b: 1 }
Note that Schema.optional
returns a PropertySignature
, which simplifies the process by eliminating the need for explicit Schema.propertySignature
usage as required in the previous example.
Renaming Properties of an Existing Schema
For existing schemas, the rename
API offers a way to systematically change property names across a schema, even within complex structures like unions.
Example: Renaming Properties in a Struct Schema
ts
import {Schema } from "@effect/schema"// Original SchemaconstoriginalSchema =Schema .Struct ({c :Schema .String ,b :Schema .Number })// Renaming the "c" property to "a"constrenamedSchema =Schema .rename (originalSchema , {c : "a" })console .log (Schema .decodeUnknownSync (renamedSchema )({c : "c",b : 1 }))// Output: { a: "c", b: 1 }
ts
import {Schema } from "@effect/schema"// Original SchemaconstoriginalSchema =Schema .Struct ({c :Schema .String ,b :Schema .Number })// Renaming the "c" property to "a"constrenamedSchema =Schema .rename (originalSchema , {c : "a" })console .log (Schema .decodeUnknownSync (renamedSchema )({c : "c",b : 1 }))// Output: { a: "c", b: 1 }
Example: Renaming Properties in Union Schemas
ts
import {Schema } from "@effect/schema"constoriginalSchema =Schema .Union (Schema .Struct ({c :Schema .String ,b :Schema .Number }),Schema .Struct ({c :Schema .String ,d :Schema .Boolean }))// Renaming the "c" property to "a" for all membersconstrenamedSchema =Schema .rename (originalSchema , {c : "a" })console .log (Schema .decodeUnknownSync (renamedSchema )({c : "c",b : 1 }))// Output: { a: "c", b: 1 }console .log (Schema .decodeUnknownSync (renamedSchema )({c : "c",d : false }))// Output: { d: false, a: 'c' }
ts
import {Schema } from "@effect/schema"constoriginalSchema =Schema .Union (Schema .Struct ({c :Schema .String ,b :Schema .Number }),Schema .Struct ({c :Schema .String ,d :Schema .Boolean }))// Renaming the "c" property to "a" for all membersconstrenamedSchema =Schema .rename (originalSchema , {c : "a" })console .log (Schema .decodeUnknownSync (renamedSchema )({c : "c",b : 1 }))// Output: { a: "c", b: 1 }console .log (Schema .decodeUnknownSync (renamedSchema )({c : "c",d : false }))// Output: { d: false, a: 'c' }
Tagged Structs
In TypeScript tags help to enhance type discrimination and pattern matching by providing a simple yet powerful way to define and recognize different data types.
What is a Tag?
A tag is a literal value added to data structures, commonly used in structs, to distinguish between various object types or variants within tagged unions. This literal acts as a discriminator, making it easier to handle and process different types of data correctly and efficiently.
Using the tag Constructor
The tag
constructor is specifically designed to create a property signature that holds a specific literal value, serving as the discriminator for object types. Here's how you can define a schema with a tag:
ts
import {Schema } from "@effect/schema"constUser =Schema .Struct ({_tag :Schema .tag ("User"),name :Schema .String ,age :Schema .Number })console .log (User .make ({name : "John",age : 44 }))/*Output:{ _tag: 'User', name: 'John', age: 44 }*/
ts
import {Schema } from "@effect/schema"constUser =Schema .Struct ({_tag :Schema .tag ("User"),name :Schema .String ,age :Schema .Number })console .log (User .make ({name : "John",age : 44 }))/*Output:{ _tag: 'User', name: 'John', age: 44 }*/
In the example above, Schema.tag("User")
attaches a _tag
property to the User
struct schema, effectively labeling objects of this struct type as "User". This label is automatically applied when using the make
method to create new instances, simplifying object creation and ensuring consistent tagging.
Simplifying Tagged Structs with TaggedStruct
The TaggedStruct
constructor streamlines the process of creating tagged structs by directly integrating the tag into the struct definition. This method provides a clearer and more declarative approach to building data structures with embedded discriminators.
ts
import {Schema } from "@effect/schema"constUser =Schema .TaggedStruct ("User", {name :Schema .String ,age :Schema .Number })// `_tag` is optionalconstuserInstance =User .make ({name : "John",age : 44 })console .log (userInstance )/*Output:{ _tag: 'User', name: 'John', age: 44 }*/
ts
import {Schema } from "@effect/schema"constUser =Schema .TaggedStruct ("User", {name :Schema .String ,age :Schema .Number })// `_tag` is optionalconstuserInstance =User .make ({name : "John",age : 44 })console .log (userInstance )/*Output:{ _tag: 'User', name: 'John', age: 44 }*/
Multiple Tags
While a primary tag is often sufficient, TypeScript allows you to define multiple tags for more complex data structuring needs. Here's an example demonstrating the use of multiple tags within a single struct:
ts
import {Schema } from "@effect/schema"constProduct =Schema .TaggedStruct ("Product", {category :Schema .tag ("Electronics"),name :Schema .String ,price :Schema .Number })// `_tag` and `category` are optionalconstproductInstance =Product .make ({name : "Smartphone",price : 999 })console .log (productInstance )/*Output:{_tag: 'Product',category: 'Electronics',name: 'Smartphone',price: 999}*/
ts
import {Schema } from "@effect/schema"constProduct =Schema .TaggedStruct ("Product", {category :Schema .tag ("Electronics"),name :Schema .String ,price :Schema .Number })// `_tag` and `category` are optionalconstproductInstance =Product .make ({name : "Smartphone",price : 999 })console .log (productInstance )/*Output:{_tag: 'Product',category: 'Electronics',name: 'Smartphone',price: 999}*/
This example showcases a product schema that not only categorizes each product under a general tag ("Product"
) but also specifies a category tag ("Electronics"
), enhancing the clarity and specificity of the data model.
instanceOf
When you need to define a schema for your custom data type defined through a class
, the most convenient and fast way is to use the Schema.instanceOf
constructor.
Example
ts
import {Schema } from "@effect/schema"classMyData {constructor(readonlyname : string) {}}// Schema.instanceOf<MyData>constMyDataSchema =Schema .instanceOf (MyData )console .log (Schema .decodeUnknownSync (MyDataSchema )(newMyData ("name")))// Output: MyData { name: 'name' }console .log (Schema .decodeUnknownSync (MyDataSchema )({name : "name" }))/*throwsParseError: Expected MyData, actual {"name":"name"}*/
ts
import {Schema } from "@effect/schema"classMyData {constructor(readonlyname : string) {}}// Schema.instanceOf<MyData>constMyDataSchema =Schema .instanceOf (MyData )console .log (Schema .decodeUnknownSync (MyDataSchema )(newMyData ("name")))// Output: MyData { name: 'name' }console .log (Schema .decodeUnknownSync (MyDataSchema )({name : "name" }))/*throwsParseError: Expected MyData, actual {"name":"name"}*/
The Schema.instanceOf
constructor is just a lightweight wrapper of the Schema.declare API, which is the primitive in @effect/schema
for declaring new custom data types.
However, note that Schema.instanceOf
can only be used for classes that expose a public constructor.
If you try to use it with classes that, for some reason, have marked the constructor as private
, you'll receive a TypeScript error:
ts
import {Schema } from "@effect/schema"classMyData {staticmake = (name : string) => newMyData (name )private constructor(readonlyname : string) {}}constArgument of type 'typeof MyData' is not assignable to parameter of type 'abstract new (...args: any) => any'. Cannot assign a 'private' constructor type to a 'public' constructor type.2345Argument of type 'typeof MyData' is not assignable to parameter of type 'abstract new (...args: any) => any'. Cannot assign a 'private' constructor type to a 'public' constructor type.MyDataSchema =Schema .instanceOf () MyData
ts
import {Schema } from "@effect/schema"classMyData {staticmake = (name : string) => newMyData (name )private constructor(readonlyname : string) {}}constArgument of type 'typeof MyData' is not assignable to parameter of type 'abstract new (...args: any) => any'. Cannot assign a 'private' constructor type to a 'public' constructor type.2345Argument of type 'typeof MyData' is not assignable to parameter of type 'abstract new (...args: any) => any'. Cannot assign a 'private' constructor type to a 'public' constructor type.MyDataSchema =Schema .instanceOf () MyData
In such cases, you cannot use Schema.instanceOf
, and you must rely on Schema.declare like this:
ts
import {Schema } from "@effect/schema"classMyData {staticmake = (name : string) => newMyData (name )private constructor(readonlyname : string) {}}constMyDataSchema =Schema .declare ((input : unknown):input isMyData =>input instanceofMyData )console .log (Schema .decodeUnknownSync (MyDataSchema )(MyData .make ("name")))// Output: MyData { name: 'name' }console .log (Schema .decodeUnknownSync (MyDataSchema )({name : "name" }))/*throwsParseError: Expected <declaration schema>, actual {"name":"name"}*/
ts
import {Schema } from "@effect/schema"classMyData {staticmake = (name : string) => newMyData (name )private constructor(readonlyname : string) {}}constMyDataSchema =Schema .declare ((input : unknown):input isMyData =>input instanceofMyData )console .log (Schema .decodeUnknownSync (MyDataSchema )(MyData .make ("name")))// Output: MyData { name: 'name' }console .log (Schema .decodeUnknownSync (MyDataSchema )({name : "name" }))/*throwsParseError: Expected <declaration schema>, actual {"name":"name"}*/
To improve the error message in case of failed decoding, remember to add annotations:
ts
constMyDataSchema =Schema .declare ((input : unknown):input isMyData =>input instanceofMyData ,{identifier : "MyData" } // annotations)console .log (Schema .decodeUnknownSync (MyDataSchema )({name : "name" }))/*throwsParseError: Expected MyData, actual {"name":"name"}*/
ts
constMyDataSchema =Schema .declare ((input : unknown):input isMyData =>input instanceofMyData ,{identifier : "MyData" } // annotations)console .log (Schema .decodeUnknownSync (MyDataSchema )({name : "name" }))/*throwsParseError: Expected MyData, actual {"name":"name"}*/
pick
The pick
static function available in each struct schema can be used to create a new Struct
by selecting particular properties from an existing Struct
.
ts
import {Schema } from "@effect/schema"constMyStruct =Schema .Struct ({a :Schema .String ,b :Schema .Number ,c :Schema .Boolean })// Schema.Struct<{ a: typeof Schema.String; c: typeof Schema.Boolean; }>constPickedSchema =MyStruct .pick ("a", "c")
ts
import {Schema } from "@effect/schema"constMyStruct =Schema .Struct ({a :Schema .String ,b :Schema .Number ,c :Schema .Boolean })// Schema.Struct<{ a: typeof Schema.String; c: typeof Schema.Boolean; }>constPickedSchema =MyStruct .pick ("a", "c")
The Schema.pick
function can be applied more broadly beyond just Struct
types, such as with unions of schemas.
However it returns a generic SchemaClass
.
Example: Picking from a Union
ts
import {Schema } from "@effect/schema"constMyUnion =Schema .Union (Schema .Struct ({a :Schema .String ,b :Schema .String ,c :Schema .String }),Schema .Struct ({a :Schema .Number ,b :Schema .Number ,d :Schema .Number }))// Schema.Schema<{ readonly a: string | number; readonly b: string | number }>constPickedSchema =MyUnion .pipe (Schema .pick ("a", "b"))
ts
import {Schema } from "@effect/schema"constMyUnion =Schema .Union (Schema .Struct ({a :Schema .String ,b :Schema .String ,c :Schema .String }),Schema .Struct ({a :Schema .Number ,b :Schema .Number ,d :Schema .Number }))// Schema.Schema<{ readonly a: string | number; readonly b: string | number }>constPickedSchema =MyUnion .pipe (Schema .pick ("a", "b"))
omit
The omit
static function available in each struct schema can be used to create a new Struct
by excluding particular properties from an existing Struct
.
ts
import {Schema } from "@effect/schema"constMyStruct =Schema .Struct ({a :Schema .String ,b :Schema .Number ,c :Schema .Boolean })// Schema.Struct<{ a: typeof Schema.String; c: typeof Schema.Boolean; }>constPickedSchema =MyStruct .omit ("b")
ts
import {Schema } from "@effect/schema"constMyStruct =Schema .Struct ({a :Schema .String ,b :Schema .Number ,c :Schema .Boolean })// Schema.Struct<{ a: typeof Schema.String; c: typeof Schema.Boolean; }>constPickedSchema =MyStruct .omit ("b")
The Schema.omit
function can be applied more broadly beyond just Struct
types, such as with unions of schemas.
However it returns a generic Schema
.
ts
import {Schema } from "@effect/schema"constMyUnion =Schema .Union (Schema .Struct ({a :Schema .String ,b :Schema .String ,c :Schema .String }),Schema .Struct ({a :Schema .Number ,b :Schema .Number ,d :Schema .Number }))// Schema<{ readonly a: string | number }>constPickedSchema =MyUnion .pipe (Schema .omit ("b"))
ts
import {Schema } from "@effect/schema"constMyUnion =Schema .Union (Schema .Struct ({a :Schema .String ,b :Schema .String ,c :Schema .String }),Schema .Struct ({a :Schema .Number ,b :Schema .Number ,d :Schema .Number }))// Schema<{ readonly a: string | number }>constPickedSchema =MyUnion .pipe (Schema .omit ("b"))
partial
The Schema.partial
operation makes all properties within a schema optional.
By default, the Schema.partial
operation adds a union with undefined
to the types. If you wish to avoid this, you can opt-out by passing a { exact: true }
argument to the Schema.partialWith
operation.
Example
ts
import {Schema } from "@effect/schema"// Schema<{ readonly a?: string | undefined; }>constschema =Schema .partial (Schema .Struct ({a :Schema .String }))Schema .decodeUnknownSync (schema )({a : "a" }) // okSchema .decodeUnknownSync (schema )({a :undefined }) // ok// Schema<{ readonly a?: string; }>constexactSchema =Schema .partialWith (Schema .Struct ({a :Schema .String }), {exact : true})Schema .decodeUnknownSync (exactSchema )({a : "a" }) // okSchema .decodeUnknownSync (exactSchema )({a :undefined })/*throws:ParseError: { readonly a?: string }└─ ["a"]└─ Expected string, actual undefined*/
ts
import {Schema } from "@effect/schema"// Schema<{ readonly a?: string | undefined; }>constschema =Schema .partial (Schema .Struct ({a :Schema .String }))Schema .decodeUnknownSync (schema )({a : "a" }) // okSchema .decodeUnknownSync (schema )({a :undefined }) // ok// Schema<{ readonly a?: string; }>constexactSchema =Schema .partialWith (Schema .Struct ({a :Schema .String }), {exact : true})Schema .decodeUnknownSync (exactSchema )({a : "a" }) // okSchema .decodeUnknownSync (exactSchema )({a :undefined })/*throws:ParseError: { readonly a?: string }└─ ["a"]└─ Expected string, actual undefined*/
required
The Schema.required
operation ensures that all properties in a schema are mandatory.
ts
import {Schema } from "@effect/schema"// Schema<{ readonly a: string; readonly b: number; }>constschema =Schema .required (Schema .Struct ({a :Schema .optionalWith (Schema .String , {exact : true }),b :Schema .optionalWith (Schema .Number , {exact : true })}))
ts
import {Schema } from "@effect/schema"// Schema<{ readonly a: string; readonly b: number; }>constschema =Schema .required (Schema .Struct ({a :Schema .optionalWith (Schema .String , {exact : true }),b :Schema .optionalWith (Schema .Number , {exact : true })}))
Extending Schemas
Spreading Struct fields
Structs expose their fields through a fields
property. This feature can be utilized to extend an existing struct with additional fields or to merge fields from another struct.
Example: Adding Fields
ts
import {Schema } from "@effect/schema"constStruct1 =Schema .Struct ({a :Schema .String ,b :Schema .String })constExtended =Schema .Struct ({...Struct1 .fields ,// other fieldsc :Schema .String ,d :Schema .String })
ts
import {Schema } from "@effect/schema"constStruct1 =Schema .Struct ({a :Schema .String ,b :Schema .String })constExtended =Schema .Struct ({...Struct1 .fields ,// other fieldsc :Schema .String ,d :Schema .String })
Example: Integrating Additional Index Signatures
ts
import {Schema } from "@effect/schema"constStruct =Schema .Struct ({a :Schema .String ,b :Schema .String })constExtended =Schema .Struct (Struct .fields ,Schema .Record ({key :Schema .String ,value :Schema .String }))
ts
import {Schema } from "@effect/schema"constStruct =Schema .Struct ({a :Schema .String ,b :Schema .String })constExtended =Schema .Struct (Struct .fields ,Schema .Record ({key :Schema .String ,value :Schema .String }))
Example: Merging Fields from Two Structs
ts
import {Schema } from "@effect/schema"constStruct1 =Schema .Struct ({a :Schema .String ,b :Schema .String })constStruct2 =Schema .Struct ({c :Schema .String ,d :Schema .String })constExtended =Schema .Struct ({...Struct1 .fields ,...Struct2 .fields })
ts
import {Schema } from "@effect/schema"constStruct1 =Schema .Struct ({a :Schema .String ,b :Schema .String })constStruct2 =Schema .Struct ({c :Schema .String ,d :Schema .String })constExtended =Schema .Struct ({...Struct1 .fields ,...Struct2 .fields })
The extend combinator
The Schema.extend
combinator offers a structured way to extend schemas, particularly useful when direct field spreading is insufficient—for instance, when you need to extend a struct with a union of structs.
Note that not all extensions are supported, and their support depends on the nature of the involved schemas:
Possible extensions include:
Schema.String
with anotherSchema.String
refinement or a string literalSchema.Number
with anotherSchema.Number
refinement or a number literalSchema.Boolean
with anotherSchema.Boolean
refinement or a boolean literal- A struct with another struct where overlapping fields support extension
- A struct with in index signature
- A struct with a union of supported schemas
- A refinement of a struct with a supported schema
- A suspend of a struct with a supported schema
Example: Extending a Struct with a Union of Structs
ts
import {Schema } from "@effect/schema"constStruct =Schema .Struct ({a :Schema .String })constUnionOfStructs =Schema .Union (Schema .Struct ({b :Schema .String }),Schema .Struct ({c :Schema .String }))constExtended =Schema .extend (Struct ,UnionOfStructs )
ts
import {Schema } from "@effect/schema"constStruct =Schema .Struct ({a :Schema .String })constUnionOfStructs =Schema .Union (Schema .Struct ({b :Schema .String }),Schema .Struct ({c :Schema .String }))constExtended =Schema .extend (Struct ,UnionOfStructs )
This example shows an attempt to extend a struct with another struct where field names overlap, leading to an error:
ts
import {Schema } from "@effect/schema"constStruct =Schema .Struct ({a :Schema .String })constOverlappingUnion =Schema .Union (Schema .Struct ({a :Schema .Number }), // duplicate keySchema .Struct ({d :Schema .String }))constExtended =Schema .extend (Struct ,OverlappingUnion )/*throws:Error: Unsupported schema or overlapping typesat path: ["a"]details: cannot extend string with number*/
ts
import {Schema } from "@effect/schema"constStruct =Schema .Struct ({a :Schema .String })constOverlappingUnion =Schema .Union (Schema .Struct ({a :Schema .Number }), // duplicate keySchema .Struct ({d :Schema .String }))constExtended =Schema .extend (Struct ,OverlappingUnion )/*throws:Error: Unsupported schema or overlapping typesat path: ["a"]details: cannot extend string with number*/
Example: Extending a refinement of Schema.String with another refinement
ts
import {Schema } from "@effect/schema"constInteger =Schema .Int .pipe (Schema .brand ("Int"))constPositive =Schema .Positive .pipe (Schema .brand ("Positive"))// Schema.Schema<number & Brand<"Positive"> & Brand<"Int">, number, never>constPositiveInteger =Schema .asSchema (Schema .extend (Positive ,Integer ))Schema .decodeUnknownSync (PositiveInteger )(-1)/*throwsParseError: Int & Brand<"Int">└─ From side refinement failure└─ Positive & Brand<"Positive">└─ Predicate refinement failure└─ Expected Positive & Brand<"Positive">, actual -1*/Schema .decodeUnknownSync (PositiveInteger )(1.1)/*throwsParseError: Int & Brand<"Int">└─ Predicate refinement failure└─ Expected Int & Brand<"Int">, actual 1.1*/
ts
import {Schema } from "@effect/schema"constInteger =Schema .Int .pipe (Schema .brand ("Int"))constPositive =Schema .Positive .pipe (Schema .brand ("Positive"))// Schema.Schema<number & Brand<"Positive"> & Brand<"Int">, number, never>constPositiveInteger =Schema .asSchema (Schema .extend (Positive ,Integer ))Schema .decodeUnknownSync (PositiveInteger )(-1)/*throwsParseError: Int & Brand<"Int">└─ From side refinement failure└─ Positive & Brand<"Positive">└─ Predicate refinement failure└─ Expected Positive & Brand<"Positive">, actual -1*/Schema .decodeUnknownSync (PositiveInteger )(1.1)/*throwsParseError: Int & Brand<"Int">└─ Predicate refinement failure└─ Expected Int & Brand<"Int">, actual 1.1*/
Composition
Combining and reusing schemas is a common requirement, the Schema.compose
combinator allows you to do just that.
It enables you to combine two schemas, Schema<B, A, R1>
and Schema<C, B, R2>
, into a single schema Schema<C, A, R1 | R2>
:
ts
import {Schema } from "@effect/schema"// Schema<readonly string[], string>constschema1 =Schema .split (",")// Schema<readonly number[], readonly string[]>constschema2 =Schema .Array (Schema .NumberFromString )// Schema<readonly number[], string>constComposedSchema =Schema .compose (schema1 ,schema2 )
ts
import {Schema } from "@effect/schema"// Schema<readonly string[], string>constschema1 =Schema .split (",")// Schema<readonly number[], readonly string[]>constschema2 =Schema .Array (Schema .NumberFromString )// Schema<readonly number[], string>constComposedSchema =Schema .compose (schema1 ,schema2 )
In this example, we have two schemas, schema1
and schema2
. The first schema, schema1
, takes a string and splits it into an array using a comma as the delimiter. The second schema, schema2
, transforms an array of strings into an array of numbers.
Now, by using the compose
combinator, we can create a new schema, ComposedSchema
, that combines the functionality of both schema1
and schema2
. This allows us to parse a string and directly obtain an array of numbers as a result.
Non-strict Option
If you need to be less restrictive when composing your schemas, i.e., when you have something like Schema<R1, A, B>
and Schema<R2, C, D>
where C
is different from B
, you can make use of the { strict: false }
option:
ts
import {Schema } from "@effect/schema"Schema .compose (// @ts-expect-errorSchema .Union (Schema .Null ,Schema .Literal ("0")),Schema .NumberFromString )// okSchema .compose (Schema .Union (Schema .Null ,Schema .Literal ("0")),Schema .NumberFromString ,{strict : false })
ts
import {Schema } from "@effect/schema"Schema .compose (// @ts-expect-errorSchema .Union (Schema .Null ,Schema .Literal ("0")),Schema .NumberFromString )// okSchema .compose (Schema .Union (Schema .Null ,Schema .Literal ("0")),Schema .NumberFromString ,{strict : false })
Declaring New Data Types
Creating schemas for new data types is crucial to defining the expected structure of information in your application. This guide explores how to declare schemas for new data types. We'll cover two important concepts: declaring schemas for primitive data types and type constructors.
Declaring Schemas for Primitive Data Types
A primitive data type represents simple values. To declare a schema for a primitive data type, like the File
type in TypeScript, we use the S.declare
constructor along with a type guard. Let's go through an example:
ts
import {Schema } from "@effect/schema"constFileFromSelf =Schema .declare ((input : unknown):input isFile =>input instanceofFile )constdecode =Schema .decodeUnknownSync (FileFromSelf )console .log (decode (newFile ([], "")))// Output: File { size: 0, type: '', name: '', lastModified: 1724774163056 }decode (null)/*throwsParseError: Expected <declaration schema>, actual null*/
ts
import {Schema } from "@effect/schema"constFileFromSelf =Schema .declare ((input : unknown):input isFile =>input instanceofFile )constdecode =Schema .decodeUnknownSync (FileFromSelf )console .log (decode (newFile ([], "")))// Output: File { size: 0, type: '', name: '', lastModified: 1724774163056 }decode (null)/*throwsParseError: Expected <declaration schema>, actual null*/
As you can see, the error message describes what went wrong but doesn't provide much information about which schema caused the error ("Expected <declaration schema>"
). To enhance the default error message, you can add annotations, particularly the identifier
, title
, and description
annotations (none of these annotations are required, but they are encouraged for good practice and can make your schema "self-documenting"). These annotations will be utilized by the messaging system to return more meaningful messages.
A "title" should be concise, while a "description" provides a more detailed explanation of the purpose of the data described by the schema.
ts
import {Schema } from "@effect/schema"constFileFromSelf =Schema .declare ((input : unknown):input isFile =>input instanceofFile ,{identifier : "FileFromSelf",description : "The `File` type in JavaScript"})constdecode =Schema .decodeUnknownSync (FileFromSelf )console .log (decode (newFile ([], "")))// Output: File { size: 0, type: '', name: '', lastModified: 1724774221857 }decode (null)/*throwsParseError: Expected FileFromSelf, actual null*/
ts
import {Schema } from "@effect/schema"constFileFromSelf =Schema .declare ((input : unknown):input isFile =>input instanceofFile ,{identifier : "FileFromSelf",description : "The `File` type in JavaScript"})constdecode =Schema .decodeUnknownSync (FileFromSelf )console .log (decode (newFile ([], "")))// Output: File { size: 0, type: '', name: '', lastModified: 1724774221857 }decode (null)/*throwsParseError: Expected FileFromSelf, actual null*/
Declaring Schemas for Type Constructors
Type constructors are generic types that take one or more types as arguments and return a new type. If you need to define a schema for a type constructor, you can use the S.declare
constructor. Let's illustrate this with a schema for ReadonlySet<A>
:
ts
import {ParseResult ,Schema } from "@effect/schema"export constMyReadonlySet = <A ,I ,R >(// Schema for the elements of the Setitem :Schema .Schema <A ,I ,R >):Schema .Schema <ReadonlySet <A >,ReadonlySet <I >,R > =>Schema .declare (// Store the schema for the elements[item ],{// Decoding functiondecode : (item ) => (input ,parseOptions ,ast ) => {if (input instanceofSet ) {// Decode the elementsconstelements =ParseResult .decodeUnknown (Schema .Array (item ))(Array .from (input .values ()),parseOptions )// Return a Set containing the parsed elementsreturnParseResult .map (elements ,(as ):ReadonlySet <A > => newSet (as ))}returnParseResult .fail (newParseResult .Type (ast ,input ))},// Encoding functionencode : (item ) => (input ,parseOptions ,ast ) => {if (input instanceofSet ) {// Encode the elementsconstelements =ParseResult .encodeUnknown (Schema .Array (item ))(Array .from (input .values ()),parseOptions )// Return a Set containing the parsed elementsreturnParseResult .map (elements ,(is ):ReadonlySet <I > => newSet (is ))}returnParseResult .fail (newParseResult .Type (ast ,input ))}},{description : `ReadonlySet<${Schema .format (item )}>`})// const setOfNumbers: S.Schema<ReadonlySet<string>, ReadonlySet<number>>constsetOfNumbers =MyReadonlySet (Schema .NumberFromString )constdecode =Schema .decodeUnknownSync (setOfNumbers )console .log (decode (newSet (["1", "2", "3"]))) // Set(3) { 1, 2, 3 }decode (null)/*throwsParseError: Expected ReadonlySet<NumberFromString>, actual null*/decode (newSet (["1", null, "3"]))/*throwsParseError: ReadonlyArray<NumberFromString>└─ [1]└─ NumberFromString└─ Encoded side transformation failure└─ Expected string, actual null*/
ts
import {ParseResult ,Schema } from "@effect/schema"export constMyReadonlySet = <A ,I ,R >(// Schema for the elements of the Setitem :Schema .Schema <A ,I ,R >):Schema .Schema <ReadonlySet <A >,ReadonlySet <I >,R > =>Schema .declare (// Store the schema for the elements[item ],{// Decoding functiondecode : (item ) => (input ,parseOptions ,ast ) => {if (input instanceofSet ) {// Decode the elementsconstelements =ParseResult .decodeUnknown (Schema .Array (item ))(Array .from (input .values ()),parseOptions )// Return a Set containing the parsed elementsreturnParseResult .map (elements ,(as ):ReadonlySet <A > => newSet (as ))}returnParseResult .fail (newParseResult .Type (ast ,input ))},// Encoding functionencode : (item ) => (input ,parseOptions ,ast ) => {if (input instanceofSet ) {// Encode the elementsconstelements =ParseResult .encodeUnknown (Schema .Array (item ))(Array .from (input .values ()),parseOptions )// Return a Set containing the parsed elementsreturnParseResult .map (elements ,(is ):ReadonlySet <I > => newSet (is ))}returnParseResult .fail (newParseResult .Type (ast ,input ))}},{description : `ReadonlySet<${Schema .format (item )}>`})// const setOfNumbers: S.Schema<ReadonlySet<string>, ReadonlySet<number>>constsetOfNumbers =MyReadonlySet (Schema .NumberFromString )constdecode =Schema .decodeUnknownSync (setOfNumbers )console .log (decode (newSet (["1", "2", "3"]))) // Set(3) { 1, 2, 3 }decode (null)/*throwsParseError: Expected ReadonlySet<NumberFromString>, actual null*/decode (newSet (["1", null, "3"]))/*throwsParseError: ReadonlyArray<NumberFromString>└─ [1]└─ NumberFromString└─ Encoded side transformation failure└─ Expected string, actual null*/
The decoding and encoding functions cannot use context (the R
type
parameter) and cannot use async effects.
Adding Annotations
When you define a new data type, some compilers like Arbitrary
or Pretty
may not know how to handle the newly defined data. For instance:
ts
import {Arbitrary ,Schema } from "@effect/schema"constFileFromSelf =Schema .declare ((input : unknown):input isFile =>input instanceofFile ,{identifier : "FileFromSelf"})// Create an Arbitrary instance for FileFromSelf schemaconstarb =Arbitrary .make (FileFromSelf )/*throws:Error: Missing annotationdetails: Generating an Arbitrary for this schema requires an "arbitrary" annotationschema (Declaration): FileFromSelf*/
ts
import {Arbitrary ,Schema } from "@effect/schema"constFileFromSelf =Schema .declare ((input : unknown):input isFile =>input instanceofFile ,{identifier : "FileFromSelf"})// Create an Arbitrary instance for FileFromSelf schemaconstarb =Arbitrary .make (FileFromSelf )/*throws:Error: Missing annotationdetails: Generating an Arbitrary for this schema requires an "arbitrary" annotationschema (Declaration): FileFromSelf*/
In such cases, you need to provide annotations to ensure proper functionality:
ts
import {Arbitrary ,FastCheck ,Pretty ,Schema } from "@effect/schema"constFileFromSelf =Schema .declare ((input : unknown):input isFile =>input instanceofFile ,{identifier : "FileFromSelf",// Provide an arbitrary function to generate random File instancesarbitrary : () => (fc ) =>fc .tuple (fc .string (),fc .string ()).map (([content ,path ]) => newFile ([content ],path )),// Provide a pretty function to generate human-readable representation of File instancespretty : () => (file ) => `File(${file .name })`})// Create an Arbitrary instance for FileFromSelf schemaconstarb =Arbitrary .make (FileFromSelf )// Generate sample files using the Arbitrary instanceconstfiles =FastCheck .sample (arb , 2)console .log (files )/*Example Output:[File { size: 5, type: '', name: 'C', lastModified: 1706435571176 },File { size: 1, type: '', name: '98Ggmc', lastModified: 1706435571176 }]*/// Create a Pretty instance for FileFromSelf schemaconstpretty =Pretty .make (FileFromSelf )// Print human-readable representation of a fileconsole .log (pretty (files [0]))// Example Output: "File(C)"
ts
import {Arbitrary ,FastCheck ,Pretty ,Schema } from "@effect/schema"constFileFromSelf =Schema .declare ((input : unknown):input isFile =>input instanceofFile ,{identifier : "FileFromSelf",// Provide an arbitrary function to generate random File instancesarbitrary : () => (fc ) =>fc .tuple (fc .string (),fc .string ()).map (([content ,path ]) => newFile ([content ],path )),// Provide a pretty function to generate human-readable representation of File instancespretty : () => (file ) => `File(${file .name })`})// Create an Arbitrary instance for FileFromSelf schemaconstarb =Arbitrary .make (FileFromSelf )// Generate sample files using the Arbitrary instanceconstfiles =FastCheck .sample (arb , 2)console .log (files )/*Example Output:[File { size: 5, type: '', name: 'C', lastModified: 1706435571176 },File { size: 1, type: '', name: '98Ggmc', lastModified: 1706435571176 }]*/// Create a Pretty instance for FileFromSelf schemaconstpretty =Pretty .make (FileFromSelf )// Print human-readable representation of a fileconsole .log (pretty (files [0]))// Example Output: "File(C)"
Recursive Schemas
The Schema.suspend
function is useful when you need to define a schema that depends on itself, like in the case of recursive data structures.
Example
In this example, the Category
schema depends on itself because it has a field subcategories
that is an array of Category
objects.
ts
import {Schema } from "@effect/schema"interfaceCategory {readonlyname : stringreadonlysubcategories :ReadonlyArray <Category >}constCategory =Schema .Struct ({name :Schema .String ,subcategories :Schema .Array (Schema .suspend (():Schema .Schema <Category > =>Category ))})
ts
import {Schema } from "@effect/schema"interfaceCategory {readonlyname : stringreadonlysubcategories :ReadonlyArray <Category >}constCategory =Schema .Struct ({name :Schema .String ,subcategories :Schema .Array (Schema .suspend (():Schema .Schema <Category > =>Category ))})
It is necessary to define the Category
type and add an explicit type
annotation because otherwise TypeScript would struggle to infer types
correctly. Without this annotation, you might encounter the error message:
ts
import {Schema } from "@effect/schema"const'Category' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.7022'Category' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.= Category Schema .Struct ({name :Schema .String ,Function implicitly has return type 'any' because it does not have a return type annotation and is referenced directly or indirectly in one of its return expressions.7024Function implicitly has return type 'any' because it does not have a return type annotation and is referenced directly or indirectly in one of its return expressions.subcategories :Schema .Array (Schema .suspend (() =>Category ))})
ts
import {Schema } from "@effect/schema"const'Category' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.7022'Category' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.= Category Schema .Struct ({name :Schema .String ,Function implicitly has return type 'any' because it does not have a return type annotation and is referenced directly or indirectly in one of its return expressions.7024Function implicitly has return type 'any' because it does not have a return type annotation and is referenced directly or indirectly in one of its return expressions.subcategories :Schema .Array (Schema .suspend (() =>Category ))})
A Helpful Pattern to Simplify Schema Definition
As we've observed, it's necessary to define an interface for the Type
of the schema to enable recursive schema definition, which can complicate things and be quite tedious.
One pattern to mitigate this is to separate the field responsible for recursion from all other fields.
ts
import {Schema } from "@effect/schema"constfields = {name :Schema .String // ...possibly other fields}// Define an interface for the Category schema, extending the Type of the defined fieldsinterfaceCategory extendsSchema .Struct .Type <typeoffields > {// Define `subcategories` using recursionreadonlysubcategories :ReadonlyArray <Category >}constCategory =Schema .Struct ({...fields , // Include the fieldssubcategories :Schema .Array (// Define `subcategories` using recursionSchema .suspend (():Schema .Schema <Category > =>Category ))})
ts
import {Schema } from "@effect/schema"constfields = {name :Schema .String // ...possibly other fields}// Define an interface for the Category schema, extending the Type of the defined fieldsinterfaceCategory extendsSchema .Struct .Type <typeoffields > {// Define `subcategories` using recursionreadonlysubcategories :ReadonlyArray <Category >}constCategory =Schema .Struct ({...fields , // Include the fieldssubcategories :Schema .Array (// Define `subcategories` using recursionSchema .suspend (():Schema .Schema <Category > =>Category ))})
Mutually Recursive Schemas
Here's an example of two mutually recursive schemas, Expression
and Operation
, that represent a simple arithmetic expression tree.
ts
import {Schema } from "@effect/schema"interfaceExpression {readonlytype : "expression"readonlyvalue : number |Operation }interfaceOperation {readonlytype : "operation"readonlyoperator : "+" | "-"readonlyleft :Expression readonlyright :Expression }constExpression =Schema .Struct ({type :Schema .Literal ("expression"),value :Schema .Union (Schema .Number ,Schema .suspend (():Schema .Schema <Operation > =>Operation ))})constOperation =Schema .Struct ({type :Schema .Literal ("operation"),operator :Schema .Literal ("+", "-"),left :Expression ,right :Expression })
ts
import {Schema } from "@effect/schema"interfaceExpression {readonlytype : "expression"readonlyvalue : number |Operation }interfaceOperation {readonlytype : "operation"readonlyoperator : "+" | "-"readonlyleft :Expression readonlyright :Expression }constExpression =Schema .Struct ({type :Schema .Literal ("expression"),value :Schema .Union (Schema .Number ,Schema .suspend (():Schema .Schema <Operation > =>Operation ))})constOperation =Schema .Struct ({type :Schema .Literal ("operation"),operator :Schema .Literal ("+", "-"),left :Expression ,right :Expression })
Recursive Types with Different Encoded and Type
Defining a recursive schema where the Encoded
type differs from the Type
type adds another layer of complexity. In such cases, we need to define two interfaces: one for the Type
type, as seen previously, and another for the Encoded
type.
Example
Let's consider an example: suppose we want to add an id
field to the Category
schema, where the schema for id
is NumberFromString
.
It's important to note that NumberFromString
is a schema that transforms a string into a number, so the Type
and Encoded
types of NumberFromString
differ, being number
and string
respectively.
When we add this field to the Category
schema, TypeScript raises an error:
ts
import {Schema } from "@effect/schema"constfields = {id :Schema .NumberFromString ,name :Schema .String }interfaceCategory extendsSchema .Struct .Type <typeoffields > {readonlysubcategories :ReadonlyArray <Category >}constCategory =Schema .Struct ({...fields ,subcategories :Schema .Array (Type 'Struct<{ subcategories: Array$<suspend<Category, Category, never>>; id: typeof NumberFromString; name: typeof String$; }>' is not assignable to type 'Schema<Category, Category, never>'. The types of 'Encoded.id' are incompatible between these types. Type 'string' is not assignable to type 'number'.2322Type 'Struct<{ subcategories: Array$<suspend<Category, Category, never>>; id: typeof NumberFromString; name: typeof String$; }>' is not assignable to type 'Schema<Category, Category, never>'. The types of 'Encoded.id' are incompatible between these types. Type 'string' is not assignable to type 'number'.Schema .suspend (():Schema .Schema <Category > =>) Category )})
ts
import {Schema } from "@effect/schema"constfields = {id :Schema .NumberFromString ,name :Schema .String }interfaceCategory extendsSchema .Struct .Type <typeoffields > {readonlysubcategories :ReadonlyArray <Category >}constCategory =Schema .Struct ({...fields ,subcategories :Schema .Array (Type 'Struct<{ subcategories: Array$<suspend<Category, Category, never>>; id: typeof NumberFromString; name: typeof String$; }>' is not assignable to type 'Schema<Category, Category, never>'. The types of 'Encoded.id' are incompatible between these types. Type 'string' is not assignable to type 'number'.2322Type 'Struct<{ subcategories: Array$<suspend<Category, Category, never>>; id: typeof NumberFromString; name: typeof String$; }>' is not assignable to type 'Schema<Category, Category, never>'. The types of 'Encoded.id' are incompatible between these types. Type 'string' is not assignable to type 'number'.Schema .suspend (():Schema .Schema <Category > =>) Category )})
This error occurs because the explicit annotation Schema.Schema<Category>
is no longer sufficient and needs to be adjusted by explicitly adding the Encoded
type:
ts
import {Schema } from "@effect/schema"constfields = {id :Schema .NumberFromString ,name :Schema .String }interfaceCategory extendsSchema .Struct .Type <typeoffields > {readonlysubcategories :ReadonlyArray <Category >}interfaceCategoryEncoded extendsSchema .Struct .Encoded <typeoffields > {readonlysubcategories :ReadonlyArray <CategoryEncoded >}constCategory =Schema .Struct ({...fields ,subcategories :Schema .Array (Schema .suspend (():Schema .Schema <Category ,CategoryEncoded > =>Category ))})
ts
import {Schema } from "@effect/schema"constfields = {id :Schema .NumberFromString ,name :Schema .String }interfaceCategory extendsSchema .Struct .Type <typeoffields > {readonlysubcategories :ReadonlyArray <Category >}interfaceCategoryEncoded extendsSchema .Struct .Encoded <typeoffields > {readonlysubcategories :ReadonlyArray <CategoryEncoded >}constCategory =Schema .Struct ({...fields ,subcategories :Schema .Array (Schema .suspend (():Schema .Schema <Category ,CategoryEncoded > =>Category ))})