@effect/schema v0.67.9
Introduction
Welcome to the documentation for @effect/schema
, a library for defining and using schemas to validate and transform data in TypeScript.
@effect/schema
allows you to define a Schema<Type, Encoded, Context>
that provides a blueprint for describing the structure and data types of your data. Once defined, you can leverage this schema to perform a range of operations, including:
Operation | Description |
---|---|
Decoding | Transforming data from an input type Encoded to an output type Type . |
Encoding | Converting data from an output type Type back to an input type Encoded . |
Asserting | Verifying that a value adheres to the schema's output type Type . |
Arbitraries | Generate arbitraries for fast-check testing. |
Pretty printing | Support pretty printing for data structures. |
JSON Schemas | Create JSON Schemas based on defined schemas. |
Equivalence | Create Equivalences based on defined schemas. |
If you're eager to learn how to define your first schema, jump straight to the Basic usage section!
The Schema Type
The Schema<Type, Encoded, Context>
type represents an immutable value that describes the structure of your data.
The Schema
type has three type parameters with the following meanings:
- Type. Represents the type of value that a schema can succeed with during decoding.
- Encoded. Represents the type of value that a schema can succeed with during encoding. By default, it's equal to
Type
if not explicitly provided. - Context. Similar to the
Effect
type, it represents the contextual data required by the schema to execute both decoding and encoding. If this type parameter isnever
(default if not explicitly provided), it means the schema has no requirements.
Examples
Schema<string>
(defaulted toSchema<string, string, never>
) represents a schema that decodes tostring
, encodes tostring
, and has no requirements.Schema<number, string>
(defaulted toSchema<number, string, never>
) represents a schema that decodes tonumber
fromstring
, encodes anumber
to astring
, and has no requirements.
!NOTE In the Effect ecosystem, you may often encounter the type parameters of
Schema
abbreviated asA
,I
, andR
respectively. This is just shorthand for the type value of type A, Input, and Requirements.
Schema
values are immutable, and all @effect/schema
functions produce new Schema
values.
Schema
values do not actually do anything, they are just values that model or describe the structure of your data.
Schema
values don't perform any actions themselves; they simply describe the structure of your data. A Schema
can be interpreted by various "compilers" into specific operations, depending on the compiler type (decoding, encoding, pretty printing, arbitraries, etc.).
Understanding Decoding and Encoding
sequenceDiagram
participant UA as unknown
participant A
participant I
participant UI as unknown
UI->>A: decodeUnknown
I->>A: decode
A->>I: encode
UA->>I: encodeUnknown
UA->>A: validate
UA->>A: is
UA->>A: asserts
We'll break down these concepts using an example with a Schema<Date, string, never>
. This schema serves as a tool to transform a string
into a Date
and vice versa.
Encoding
When we talk about "encoding," we are referring to the process of changing a Date
into a string
. To put it simply, it's the act of converting data from one format to another.
Decoding
Conversely, "decoding" entails transforming a string
back into a Date
. It's essentially the reverse operation of encoding, where data is returned to its original form.
Decoding From Unknown
Decoding from unknown
involves two key steps:
Checking: Initially, we verify that the input data (which is of the
unknown
type) matches the expected structure. In our specific case, this means ensuring that the input is indeed astring
.Decoding: Following the successful check, we proceed to convert the
string
into aDate
. This process completes the decoding operation, where the data is both validated and transformed.
Encoding From Unknown
Encoding from unknown
involves two key steps:
Checking: Initially, we verify that the input data (which is of the
unknown
type) matches the expected structure. In our specific case, this means ensuring that the input is indeed aDate
.Encoding: Following the successful check, we proceed to convert the
Date
into astring
. This process completes the encoding operation, where the data is both validated and transformed.
!NOTE As a general rule, schemas should be defined such that encode + decode return the original value.
Recap
- Decoding: Used for parsing data from external sources where you have no control over the data format.
- Encoding: Used when sending data out to external sources, converting it to a format that is expected by those sources.
For instance, when working with forms in the frontend, you often receive untyped data in the form of strings. This data can be tampered with and does not natively support arrays or booleans. Decoding helps you validate and parse this data into more useful types like numbers, dates, and arrays. Encoding allows you to convert these types back into the string format expected by forms.
By understanding these processes, you can ensure that your data handling is robust and reliable, converting data safely between different formats.
The Rule of Schemas: Keeping Encode and Decode in Sync
When working with schemas, there's an important rule to keep in mind: your schemas should be crafted in a way that when you perform both encoding and decoding operations, you should end up with the original value.
In simpler terms, if you encode a value and then immediately decode it, the result should match the original value you started with. This rule ensures that your data remains consistent and reliable throughout the encoding and decoding process.
Requirements
- TypeScript 5.0 or newer
- The
strict
flag enabled in yourtsconfig.json
file - The
exactOptionalPropertyTypes
flag enabled in yourtsconfig.json
file{ // ... "compilerOptions": { // ... "strict": true, "exactOptionalPropertyTypes": true } }
- Additionally, make sure to install the following packages, as they are peer dependencies. Note that some package managers might not install peer dependencies by default, so you need to install them manually:
effect
package (peer dependency)- fast-check package (peer dependency)
Understanding exactOptionalPropertyTypes
The @effect/schema
library takes advantage of the exactOptionalPropertyTypes
option of tsconfig.json
. This option affects how optional properties are typed (to learn more about this option, you can refer to the official TypeScript documentation).
Let's delve into this with an example.
With exactOptionalPropertyTypes
Enabled
import { Schema } from "@effect/schema"
const Person = Schema.Struct({
name: Schema.optional(Schema.String.pipe(Schema.nonEmpty()), {
exact: true
})
})
/*
type Type = {
readonly name?: string; // the type is strict (no `| undefined`)
}
*/
type Type = Schema.Schema.Type<typeof Person>
Schema.decodeSync(Person)({ name: undefined })
/*
TypeScript Error:
Argument of type '{ name: undefined; }' is not assignable to parameter of type '{ readonly name?: string; }' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties.
Types of property 'name' are incompatible.
Type 'undefined' is not assignable to type 'string'.ts(2379)
*/
Here, notice that the type of name
is "exact" (string
), which means the type checker will catch any attempt to assign an invalid value (like undefined
).
With exactOptionalPropertyTypes
Disabled
If, for some reason, you can't enable the exactOptionalPropertyTypes
option (perhaps due to conflicts with other third-party libraries), you can still use @effect/schema
. However, there will be a mismatch between the types and the runtime behavior:
import { Schema } from "@effect/schema"
const Person = Schema.Struct({
name: Schema.optional(Schema.String.pipe(Schema.nonEmpty()), {
exact: true
})
})
/*
type Type = {
readonly name?: string | undefined; // the type is widened to string | undefined
}
*/
type Type = Schema.Schema.Type<typeof Person>
Schema.decodeSync(Person)({ name: undefined }) // No type error, but a decoding failure occurs
/*
Error: { name?: a non empty string }
└─ ["name"]
└─ a non empty string
└─ From side refinement failure
└─ Expected a string, actual undefined
*/
In this case, the type of name
is widened to string | undefined
, which means the type checker won't catch the invalid value (undefined
). However, during decoding, you'll encounter an error, indicating that undefined
is not allowed.
Getting started
To install the alpha version:
npm install @effect/schema
Additionally, make sure to install the following packages, as they are peer dependencies. Note that some package managers might not install peer dependencies by default, so you need to install them manually:
effect
package (peer dependency)
!WARNING This package is primarily published to receive early feedback and for contributors, during this development phase we cannot guarantee the stability of the APIs, consider each minor release to contain breaking changes.
Once you have installed the library, you can import the necessary types and functions from the @effect/schema/Schema
module.
Example (Namespace Import)
import * as Schema from "@effect/schema/Schema"
Example (Named Import)
import { Schema } from "@effect/schema"
Defining a schema
One common way to define a Schema
is by utilizing the struct
constructor provided by @effect/schema
. This function allows you to create a new Schema
that outlines an object with specific properties. Each property in the object is defined by its own Schema
, which specifies the data type and any validation rules.
For example, consider the following Schema
that describes a person object with a name
property of type string
and an age
property of type number
:
import { Schema } from "@effect/schema"
const Person = Schema.Struct({
name: Schema.String,
age: Schema.Number
})
!NOTE It's important to note that by default, most constructors exported by
@effect/schema
returnreadonly
types. For instance, in thePerson
schema above, the resulting type would be{ readonly name: string; readonly age: number; }
.
Extracting Inferred Types
Type
Once you've defined a Schema<A, I, R>
, you can extract the inferred type A
, which represents the data described by the schema, in two ways:
- Using the
Schema.Schema.Type
utility. - Using the
Type
field defined on your schema.
For example, you can extract the inferred type of a Person
object as demonstrated below:
import { Schema } from "@effect/schema"
const Person = Schema.Struct({
name: Schema.String,
age: Schema.NumberFromString
})
// 1. Using the Schema.Type utility
type Person = Schema.Schema.Type<typeof Person>
/*
Equivalent to:
interface Person {
readonly name: string;
readonly age: number;
}
*/
// 2. Using the `Type` field
type Person2 = typeof Person.Type
Alternatively, you can define the Person
type using the interface
keyword:
interface Person extends Schema.Schema.Type<typeof Person> {}
/*
Equivalent to:
type Person {
readonly name: string;
readonly age: number;
}
*/
Both approaches yield the same result, but using an interface provides benefits such as performance advantages and improved readability.
Encoded
In cases where in a Schema<A, I>
the I
type differs from the A
type, you can also extract the inferred I
type using the Schema.Encoded
utility (or the Encoded
field defined on your schema).
import { Schema } from "@effect/schema"
const Person = Schema.Struct({
name: Schema.String,
age: Schema.NumberFromString
})
// 1. Using the Schema.Encoded utility
type PersonEncoded = Schema.Schema.Encoded<typeof Person>
/*
type PersonEncoded = {
readonly name: string;
readonly age: string;
}
*/
// 2. Using the `Encoded` field
type PersonEncoded2 = typeof Person.Encoded
Context
You can also extract the inferred type R
that represents the context described by the schema using the Schema.Context
utility:
import { Schema } from "@effect/schema"
const Person = Schema.Struct({
name: Schema.String,
age: Schema.NumberFromString
})
// type PersonContext = never
type PersonContext = Schema.Schema.Context<typeof Person>
Advanced extracting Inferred Types
To create a schema with an opaque type, you can use the following technique that re-declares the schema:
import { Schema } from "@effect/schema"
const _Person = Schema.Struct({
name: Schema.String,
age: Schema.Number
})
interface Person extends Schema.Schema.Type<typeof _Person> {}
// Re-declare the schema to create a schema with an opaque type
const Person: Schema.Schema<Person> = _Person
Alternatively, you can use the Class
APIs (see the Class section below for more details).
Note that the technique shown above becomes more complex when the schema is defined such that A
is different from I
. For example:
import { Schema } from "@effect/schema"
const _Person = Schema.Struct({
name: Schema.String,
age: Schema.NumberFromString
})
interface Person extends Schema.Schema.Type<typeof _Person> {}
interface PersonEncoded extends Schema.Schema.Encoded<typeof _Person> {}
// Re-declare the schema to create a schema with an opaque type
const Person: Schema.Schema<Person, PersonEncoded> = _Person
In this case, the field "age"
is of type string
in the Encoded
type of the schema and is of type number
in the Type
type of the schema. Therefore, we need to define two interfaces (PersonEncoded
and Person
) and use both to redeclare our final schema Person
.
Decoding From Unknown Values
When working with unknown data types in TypeScript, decoding them into a known structure can be challenging. Luckily, @effect/schema
provides several functions to help with this process. Let's explore how to decode unknown values using these functions.
Using decodeUnknown*
Functions
The @effect/schema/Schema
module offers a variety of decodeUnknown*
functions, each tailored for different decoding scenarios:
decodeUnknownSync
: Synchronously decodes a value and throws an error if parsing fails.decodeUnknownOption
: Decodes a value and returns anOption
type.decodeUnknownEither
: Decodes a value and returns anEither
type.decodeUnknownPromise
: Decodes a value and returns aPromise
.decodeUnknown
: Decodes a value and returns anEffect
.
Example (Using decodeUnknownSync
)
Let's begin with an example using the decodeUnknownSync
function. This function is useful when you want to parse a value and immediately throw an error if the parsing fails.
import { Schema } from "@effect/schema"
const Person = Schema.Struct({
name: Schema.String,
age: Schema.Number
})
// Simulate an unknown input
const input: unknown = { name: "Alice", age: 30 }
console.log(Schema.decodeUnknownSync(Person)(input))
// Output: { name: 'Alice', age: 30 }
console.log(Schema.decodeUnknownSync(Person)(null))
/*
throws:
Error: Expected { readonly name: string; readonly age: number }, actual null
*/
Example (Using decodeUnknownEither
)
Now, let's see how to use the decodeUnknownEither
function, which returns an Either
type representing success or failure.
import { Schema } from "@effect/schema"
import { Either } from "effect"
const Person = Schema.Struct({
name: Schema.String,
age: Schema.Number
})
const decode = Schema.decodeUnknownEither(Person)
// Simulate an unknown input
const input: unknown = { name: "Alice", age: 30 }
const result1 = decode(input)
if (Either.isRight(result1)) {
console.log(result1.right)
/*
Output:
{ name: "Alice", age: 30 }
*/
}
const result2 = decode(null)
if (Either.isLeft(result2)) {
console.log(result2.left)
/*
Output:
{
_id: 'ParseError',
message: 'Expected { readonly name: string; readonly age: number }, actual null'
}
*/
}
The decode
function returns an Either<A, ParseError>
, where ParseError
is defined as follows:
interface ParseError {
readonly _tag: "ParseError"
readonly error: ParseIssue
}
Here, ParseIssue
represents an error that might occur during the parsing process. It is wrapped in a tagged error to make it easier to catch errors using Effect.catchTag
. The result Either<A, ParseError>
contains the inferred data type described by the schema. A successful parse yields a Right
value with the parsed data A
, while a failed parse results in a Left
value containing a ParseError
.
Handling Async Transformations
When your schema involves asynchronous transformations, neither the decodeUnknownSync
nor the decodeUnknownEither
functions will work for you. In such cases, you must turn to the decodeUnknown
function, which returns an Effect
.
import { Schema } from "@effect/schema"
import { Effect } from "effect"
const PersonId = Schema.Number
const Person = Schema.Struct({
id: PersonId,
name: Schema.String,
age: Schema.Number
})
const asyncSchema = Schema.transformOrFail(PersonId, Person, {
// Simulate an async transformation
decode: (id) =>
Effect.succeed({ id, name: "name", age: 18 }).pipe(
Effect.delay("10 millis")
),
encode: (person) => Effect.succeed(person.id).pipe(Effect.delay("10 millis"))
})
const syncParsePersonId = Schema.decodeUnknownEither(asyncSchema)
console.log(JSON.stringify(syncParsePersonId(1), null, 2))
/*
Output:
{
"_id": "Either",
"_tag": "Left",
"left": {
"_id": "ParseError",
"message": "(number <-> { readonly id: number; readonly name: string; readonly age: number })\n└─ cannot be be resolved synchronously, this is caused by using runSync on an effect that performs async work"
}
}
*/
const asyncParsePersonId = Schema.decodeUnknown(asyncSchema)
Effect.runPromise(asyncParsePersonId(1)).then(console.log)
/*
Output:
{ id: 1, name: 'name', age: 18 }
*/
As shown in the code above, the first approach returns a Forbidden
error, indicating that using decodeUnknownEither
with an async transformation is not allowed. However, the second approach works as expected, allowing you to handle async transformations and return the desired result.
Excess properties
When using a Schema
to parse a value, by default any properties that are not specified in the Schema
will be stripped out from the output. This is because the Schema
is expecting a specific shape for the parsed value, and any excess properties do not conform to that shape.
However, you can use the onExcessProperty
option (default value: "ignore"
) to trigger a parsing error. This can be particularly useful in cases where you need to detect and handle potential errors or unexpected values.
Here's an example of how you might use onExcessProperty
set to "error"
:
import { Schema } from "@effect/schema"
const Person = Schema.Struct({
name: Schema.String,
age: Schema.Number
})
console.log(
Schema.decodeUnknownSync(Person)({
name: "Bob",
age: 40,
email: "bob@example.com"
})
)
/*
Output:
{ name: 'Bob', age: 40 }
*/
Schema.decodeUnknownSync(Person)(
{
name: "Bob",
age: 40,
email: "bob@example.com"
},
{ onExcessProperty: "error" }
)
/*
throws
Error: { readonly name: string; readonly age: number }
└─ ["email"]
└─ is unexpected, expected "name" | "age"
*/
If you want to allow excess properties to remain, you can use onExcessProperty
set to "preserve"
:
import { Schema } from "@effect/schema"
const Person = Schema.Struct({
name: Schema.String,
age: Schema.Number
})
console.log(
Schema.decodeUnknownSync(Person)(
{
name: "Bob",
age: 40,
email: "bob@example.com"
},
{ onExcessProperty: "preserve" }
)
)
/*
{ email: 'bob@example.com', name: 'Bob', age: 40 }
*/
!NOTE The
onExcessProperty
anderror
options also affect encoding.
All errors
The errors
option allows you to receive all parsing errors when attempting to parse a value using a schema. By default only the first error is returned, but by setting the errors
option to "all"
, you can receive all errors that occurred during the parsing process. This can be useful for debugging or for providing more comprehensive error messages to the user.
Here's an example of how you might use errors
:
import { Schema } from "@effect/schema"
const Person = Schema.Struct({
name: Schema.String,
age: Schema.Number
})
Schema.decodeUnknownSync(Person)(
{
name: "Bob",
age: "abc",
email: "bob@example.com"
},
{ errors: "all", onExcessProperty: "error" }
)
/*
throws
Error: { readonly name: string; readonly age: number }
├─ ["email"]
│ └─ is unexpected, expected "name" | "age"
└─ ["age"]
└─ Expected a number, actual "abc"
*/
!NOTE The
onExcessProperty
anderror
options also affect encoding.
Encoding
The @effect/schema/Schema
module provides several encode*
functions to encode data according to a schema:
encodeSync
: Synchronously encodes data and throws an error if encoding fails.encodeOption
: Encodes data and returns anOption
type.encodeEither
: Encodes data and returns anEither
type representing success or failure.encodePromise
: Encodes data and returns aPromise
.encode
: Encodes data and returns anEffect
.
Let's consider an example where we have a schema for a Person
object with a name
property of type string
and an age
property of type number
.
import * as S from "@effect/schema/Schema"
import { Schema } from "@effect/schema"
// Age is a schema that can decode a string to a number and encode a number to a string
const Age = Schema.NumberFromString
const Person = Schema.Struct({
name: Schema.NonEmpty,
age: Age
})
console.log(Schema.encodeSync(Person)({ name: "Alice", age: 30 }))
// Output: { name: 'Alice', age: '30' }
console.log(Schema.encodeSync(Person)({ name: "", age: 30 }))
/*
throws:
Error: { readonly name: NonEmpty; readonly age: NumberFromString }
└─ ["name"]
└─ NonEmpty
└─ Predicate refinement failure
└─ Expected NonEmpty (a non empty string), actual ""
*/
Note that during encoding, the number value 30
was converted to a string "30"
.
!NOTE The
onExcessProperty
anderror
options also affect encoding.
Handling Unsupported Encoding
Although it is generally recommended to define schemas that support both decoding and encoding, there are situations where encoding support might be impossible. In such cases, the Forbidden
error can be used to handle unsupported encoding.
Here is an example of a transformation that never fails during decoding. It returns an Either
containing either the decoded value or the original input. For encoding, it is reasonable to not support it and use Forbidden
as the result.
import { ParseResult, Schema } from "@effect/schema"
import { Either } from "effect"
// Define a schema that safely decodes to Either type
export const SafeDecode = <A, I>(self: Schema.Schema<A, I, never>) => {
const decodeUnknownEither = Schema.decodeUnknownEither(self)
return Schema.transformOrFail(
Schema.Unknown,
Schema.EitherFromSelf({
left: Schema.Unknown,
right: Schema.typeSchema(self)
}),
{
strict: true,
decode: (input) =>
ParseResult.succeed(
Either.mapLeft(decodeUnknownEither(input), () => input)
),
encode: (actual, _, ast) =>
Either.match(actual, {
onLeft: () =>
ParseResult.fail(
new ParseResult.Forbidden(ast, actual, "cannot encode a Left")
),
onRight: ParseResult.succeed
})
}
)
}
Explanation
Decoding: The
SafeDecode
function ensures that decoding never fails. It wraps the decoded value in anEither
, where a successful decoding results in aRight
and a failed decoding results in aLeft
containing the original input.Encoding: The encoding process uses the
Forbidden
error to indicate that encoding aLeft
value is not supported. OnlyRight
values are successfully encoded.
Formatting Errors
When you're working with Effect Schema and encounter errors during decoding, or encoding functions, you can format these errors in two different ways: using the TreeFormatter
or the ArrayFormatter
.
TreeFormatter (default)
The TreeFormatter
is the default method for formatting errors. It organizes errors in a tree structure, providing a clear hierarchy of issues.
Here's an example of how it works:
import { Schema, TreeFormatter } from "@effect/schema"
import { Either } from "effect"
const Person = Schema.Struct({
name: Schema.String,
age: Schema.Number
})
const result = Schema.decodeUnknownEither(Person)({})
if (Either.isLeft(result)) {
console.error("Decoding failed:")
console.error(TreeFormatter.formatErrorSync(result.left))
}
/*
Decoding failed:
{ readonly name: string; readonly age: number }
└─ ["name"]
└─ is missing
*/
In this example, the tree error message is structured as follows:
{ name: string; age: number }
represents the schema, providing a visual representation of the expected structure. This can be customized using annotations, such as setting theidentifier
annotation.["name"]
indicates the offending property, in this case, the"name"
property.is missing
represents the specific error for the"name"
property.
ParseIssueTitle Annotation
When a decoding or encoding operation fails, it's useful to have additional details in the default error message returned by TreeFormatter
to understand exactly which value caused the operation to fail. To achieve this, you can set an annotation that depends on the value undergoing the operation and can return an excerpt of it, making it easier to identify the problematic value. A common scenario is when the entity being validated has an id
field. The ParseIssueTitle
annotation facilitates this kind of analysis during error handling.
The type of the annotation is:
export type ParseIssueTitleAnnotation = (
issue: ParseIssue
) => string | undefined
If you set this annotation on a schema and the provided function returns a string
, then that string is used as the title by TreeFormatter
, unless a message
annotation (which has the highest priority) has also been set. If the function returns undefined
, then the default title used by TreeFormatter
is determined with the following priorities:
identifier
title
description
ast.toString()
Example
import type { ParseResult } from "@effect/schema"
import { Schema } from "@effect/schema"
const getOrderItemId = ({ actual }: ParseResult.ParseIssue) => {
if (Schema.is(Schema.Struct({ id: Schema.String }))(actual)) {
return `OrderItem with id: ${actual.id}`
}
}
const OrderItem = Schema.Struct({
id: Schema.String,
name: Schema.String,
price: Schema.Number
}).annotations({
identifier: "OrderItem",
parseIssueTitle: getOrderItemId
})
const getOrderId = ({ actual }: ParseResult.ParseIssue) => {
if (Schema.is(Schema.Struct({ id: Schema.Number }))(actual)) {
return `Order with id: ${actual.id}`
}
}
const Order = Schema.Struct({
id: Schema.Number,
name: Schema.String,
items: Schema.Array(OrderItem)
}).annotations({
identifier: "Order",
parseIssueTitle: getOrderId
})
const decode = Schema.decodeUnknownSync(Order, { errors: "all" })
// No id available, so the `identifier` annotation is used as the title
decode({})
/*
throws
Error: Order
├─ ["id"]
│ └─ is missing
├─ ["name"]
│ └─ is missing
└─ ["items"]
└─ is missing
*/
// An id is available, so the `parseIssueTitle` annotation is used as the title
decode({ id: 1 })
/*
throws
Error: Order with id: 1
├─ ["name"]
│ └─ is missing
└─ ["items"]
└─ is missing
*/
decode({ id: 1, items: [{ id: "22b", price: "100" }] })
/*
throws
Error: Order with id: 1
├─ ["name"]
│ └─ is missing
└─ ["items"]
└─ ReadonlyArray<OrderItem>
└─ [0]
└─ OrderItem with id: 22b
├─ ["name"]
│ └─ is missing
└─ ["price"]
└─ Expected a number, actual "100"
*/
In the examples above, we can see how the parseIssueTitle
annotation helps provide meaningful error messages when decoding fails.
ArrayFormatter
The ArrayFormatter
is an alternative way to format errors, presenting them as an array of issues. Each issue contains properties such as _tag
, path
, and message
.
Here's an example of how it works:
import { ArrayFormatter, Schema } from "@effect/schema"
import * as Either from "effect/Either"
const Person = Schema.Struct({
name: Schema.String,
age: Schema.Number
})
const result = Schema.decodeUnknownEither(Person)(
{ name: 1, foo: 2 },
{ errors: "all", onExcessProperty: "error" }
)
if (Either.isLeft(result)) {
console.error("Parsing failed:")
console.error(ArrayFormatter.formatErrorSync(result.left))
}
/*
Parsing failed:
[
{
_tag: 'Unexpected',
path: [ 'foo' ],
message: 'is unexpected, expected "name" | "age"'
},
{
_tag: 'Type',
path: [ 'name' ],
message: 'Expected a string, actual 1'
},
{ _tag: 'Missing', path: [ 'age' ], message: 'is missing' }
]
*/
Assertions
The is
function provided by the @effect/schema/Schema
module represents a way of verifying that a value conforms to a given Schema
. is
is a refinement that takes a value of type unknown
as an argument and returns a boolean
indicating whether or not the value conforms to the Schema
.
import { Schema } from "@effect/schema"
const Person = Schema.Struct({
name: Schema.String,
age: Schema.Number
})
/*
const isPerson: (a: unknown, options?: ParseOptions | undefined) => a is {
readonly name: string;
readonly age: number;
}
*/
const isPerson = Schema.is(Person)
console.log(isPerson({ name: "Alice", age: 30 })) // true
console.log(isPerson(null)) // false
console.log(isPerson({})) // false
The asserts
function takes a Schema
and returns a function that takes an input value and checks if it matches the schema. If it does not match the schema, it throws an error with a comprehensive error message.
import { Schema } from "@effect/schema"
const Person = Schema.Struct({
name: Schema.String,
age: Schema.Number
})
// equivalent to: (input: unknown, options?: ParseOptions) => asserts input is { readonly name: string; readonly age: number; }
const assertsPerson: Schema.Schema.ToAsserts<typeof Person> =
Schema.asserts(Person)
try {
assertsPerson({ name: "Alice", age: "30" })
} catch (e) {
console.error("The input does not match the schema:")
console.error(e)
}
/*
The input does not match the schema:
Error: { readonly name: string; readonly age: number }
└─ ["age"]
└─ Expected a number, actual "30"
*/
// this will not throw an error
assertsPerson({ name: "Alice", age: 30 })
Using fast-check Arbitraries
The make
function provided by the @effect/schema/Arbitrary
module represents a way of generating random values that conform to a given Schema
. This can be useful for testing purposes, as it allows you to generate random test data that is guaranteed to be valid according to the Schema
.
import { Arbitrary, FastCheck, Schema } from "@effect/schema"
const Person = Schema.Struct({
name: Schema.String,
age: Schema.String.pipe(Schema.compose(Schema.NumberFromString), Schema.int())
})
/*
FastCheck.Arbitrary<{
readonly name: string;
readonly age: number;
}>
*/
const PersonArbitraryType = Arbitrary.make(Person)
console.log(FastCheck.sample(PersonArbitraryType, 2))
/*
Output:
[ { name: 'iP=!', age: -6 }, { name: '', age: 14 } ]
*/
/*
Arbitrary for the "Encoded" type:
FastCheck.Arbitrary<{
readonly name: string;
readonly age: string;
}>
*/
const PersonArbitraryEncoded = Arbitrary.make(Schema.encodedSchema(Person))
console.log(FastCheck.sample(PersonArbitraryEncoded, 2))
/*
Output:
[ { name: '{F', age: '$"{|' }, { name: 'nB}@BK', age: '^V+|W!Z' } ]
*/
Customizations
You can customize the output by using the arbitrary
annotation:
import { Arbitrary, FastCheck, Schema } from "@effect/schema"
const schema = Schema.Number.annotations({
arbitrary: () => (fc) => fc.nat()
})
const arb = Arbitrary.make(schema)
console.log(FastCheck.sample(arb, 2))
// Output: [ 1139348969, 749305462 ]
!WARNING Note that when customizing any schema, any filter preceding the customization will be lost, only filters following the customization will be respected.
Example
import { Arbitrary, FastCheck, Schema } from "@effect/schema"
const bad = Schema.Number.pipe(Schema.positive()).annotations({
arbitrary: () => (fc) => fc.integer()
})
console.log(FastCheck.sample(Arbitrary.make(bad), 2))
// Example Output: [ -1600163302, -6 ]
const good = Schema.Number.annotations({
arbitrary: () => (fc) => fc.integer()
}).pipe(Schema.positive())
console.log(FastCheck.sample(Arbitrary.make(good), 2))
// Example Output: [ 7, 1518247613 ]
Pretty print
The make
function provided by the @effect/schema/Pretty
module represents a way of pretty-printing values that conform to a given Schema
.
You can use the make
function to create a human-readable string representation of a value that conforms to a Schema
. This can be useful for debugging or logging purposes, as it allows you to easily inspect the structure and data types of the value.
import { Pretty, Schema } from "@effect/schema"
const Person = Schema.Struct({
name: Schema.String,
age: Schema.Number
})
const PersonPretty = Pretty.make(Person)
// returns a string representation of the object
console.log(PersonPretty({ name: "Alice", age: 30 }))
/*
Output:
'{ "name": "Alice", "age": 30 }'
*/
Customizations
You can customize the output using the pretty
annotation:
import { Pretty, Schema } from "@effect/schema"
const schema = Schema.Number.annotations({
pretty: () => (n) => `my format: ${n}`
})
console.log(Pretty.make(schema)(1)) // my format: 1
Generating JSON Schemas
The make
function from the @effect/schema/JSONSchema
module enables you to create a JSON Schema based on a defined schema:
import { JSONSchema, Schema } from "@effect/schema"
const Person = Schema.Struct({
name: Schema.NonEmpty,
age: Schema.Number
})
const jsonSchema = JSONSchema.make(Person)
console.log(JSON.stringify(jsonSchema, null, 2))
/*
Output:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": [
"age",
"name"
],
"properties": {
"age": {
"type": "number",
"description": "a number",
"title": "number"
},
"name": {
"type": "string",
"description": "a non empty string",
"title": "NonEmpty",
"minLength": 1
}
},
"additionalProperties": false
}
*/
In this example, we have created a schema for a "Person" with a name (a non-empty string) and an age (a number). We then use the JSONSchema.make
function to generate the corresponding JSON Schema.
Note that JSONSchema.make
attempts to produce the optimal JSON Schema for the input part of the decoding phase. This means that starting from the most nested schema, it traverses the chain, including each refinement, and stops at the first transformation found.
For instance, if we modify the schema of the age
field:
import { JSONSchema, Schema } from "@effect/schema"
const Person = Schema.Struct({
name: Schema.NonEmpty,
age: Schema.Number.pipe(
// refinement, will be included in the generated JSON Schema
Schema.int(),
// transformation, will be excluded in the generated JSON Schema
Schema.clamp(1, 10)
)
})
const jsonSchema = JSONSchema.make(Person)
console.log(JSON.stringify(jsonSchema, null, 2))
We can see that the new JSON Schema generated for the age
field is of type "integer"
, retaining the useful refinement (being an integer) and excluding the transformation (clamping between 1
and 10
):
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["name", "age"],
"properties": {
"name": {
"type": "string",
"description": "a non empty string",
"title": "NonEmpty",
"minLength": 1
},
"age": {
"type": "integer",
"description": "an integer",
"title": "integer"
}
},
"additionalProperties": false
}
Identifier Annotations
You can enhance your schemas with identifier
annotations. If you do, your schema will be included within a "definitions" object property on the root and referenced from there:
import { JSONSchema, Schema } from "@effect/schema"
const Name = Schema.String.annotations({ identifier: "Name" })
const Age = Schema.Number.annotations({ identifier: "Age" })
const Person = Schema.Struct({
name: Name,
age: Age
})
const jsonSchema = JSONSchema.make(Person)
console.log(JSON.stringify(jsonSchema, null, 2))
/*
Output:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": [
"name",
"age"
],
"properties": {
"name": {
"$ref": "#/$defs/Name"
},
"age": {
"$ref": "#/$defs/Age"
}
},
"additionalProperties": false,
"$defs": {
"Name": {
"type": "string",
"description": "a string",
"title": "string"
},
"Age": {
"type": "number",
"description": "a number",
"title": "number"
}
}
}
*/
This technique helps organize your JSON Schema by creating separate definitions for each identifier annotated schema, making it more readable and maintainable.
Standard JSON Schema Annotations
Standard JSON Schema annotations such as title
, description
, default
, and Examples
are supported:
import { JSONSchema, Schema } from "@effect/schema"
const schema = Schema.Struct({
foo: Schema.optional(
Schema.String.annotations({
description: "an optional string field",
title: "foo",
examples: ["a", "b"]
}).pipe(Schema.compose(Schema.Trim)),
{
default: () => ""
}
).annotations({ description: "a required, trimmed string field" })
})
// Generate a JSON Schema for the input part
console.log(JSON.stringify(JSONSchema.make(schema), null, 2))
/*
Output:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": [],
"properties": {
"foo": {
"type": "string",
"description": "an optional string field",
"title": "foo",
"examples": [
"a",
"b"
]
}
},
"additionalProperties": false,
"title": "Struct (Encoded side)"
}
*/
// Generate a JSON Schema for the output part
console.log(JSON.stringify(JSONSchema.make(Schema.typeSchema(schema)), null, 2))
/*
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": [
"foo"
],
"properties": {
"foo": {
"type": "string",
"description": "a required string field",
"title": "Trimmed",
"pattern": "^.*[a-zA-Z0-9]+.*$"
}
},
"additionalProperties": false,
"title": "Struct (Type side)"
}
*/
Recursive and Mutually Recursive Schemas
Recursive and mutually recursive schemas are supported, but in these cases, identifier annotations are required:
import { JSONSchema, Schema } from "@effect/schema"
interface Category {
readonly name: string
readonly categories: ReadonlyArray<Category>
}
const schema = Schema.Struct({
name: Schema.String,
categories: Schema.Array(
Schema.suspend((): Schema.Schema<Category> => schema)
)
}).annotations({ identifier: "Category" })
const jsonSchema = JSONSchema.make(schema)
console.log(JSON.stringify(jsonSchema, null, 2))
/*
Output:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$ref": "#/$defs/Category",
"$defs": {
"Category": {
"type": "object",
"required": [
"name",
"categories"
],
"properties": {
"name": {
"type": "string",
"description": "a string",
"title": "string"
},
"categories": {
"type": "array",
"items": {
"$ref": "#/$defs/Category"
}
}
},
"additionalProperties": false
}
}
}
*/
In the example above, we define a schema for a "Category" that can contain a "name" (a string) and an array of nested "categories." To support recursive definitions, we use the S.suspend
function and identifier annotations to name our schema.
This ensures that the JSON Schema properly handles the recursive structure and creates distinct definitions for each annotated schema, improving readability and maintainability.
Custom JSON Schema Annotations
When defining a refinement (e.g., through the filter
function), you can attach a JSON Schema annotation to your schema containing a JSON Schema "fragment" related to this particular refinement. This fragment will be used to generate the corresponding JSON Schema. Note that if the schema consists of more than one refinement, the corresponding annotations will be merged.
Note:
The
jsonSchema
property is intentionally defined as a generic object. This allows it to describe non-standard extensions. As a result, the responsibility of enforcing type constraints is left to you, the user. If you prefer stricter type enforcement or need to support non-standard extensions, you can introduce asatisfies
constraint on the object literal. This constraint should be used in conjunction with the typing library of your choice.In the following example, we've used the
@types/json-schema
package to provide TypeScript definitions for JSON Schema. This approach not only ensures type correctness but also enables autocomplete suggestions in your IDE.
import { JSONSchema, Schema } from "@effect/schema"
import type { JSONSchema7 } from "json-schema"
// Simulate one or more refinements
const Positive = Schema.Number.pipe(
Schema.filter((n) => n > 0, {
jsonSchema: { minimum: 0 } // `jsonSchema` is a generic object; you can add any key-value pair without type errors or autocomplete suggestions.
})
)
const schema = Positive.pipe(
Schema.filter((n) => n <= 10, {
jsonSchema: { maximum: 10 } satisfies JSONSchema7 // Now `jsonSchema` is constrained to fulfill the JSONSchema7 type; incorrect properties will trigger type errors, and you'll get autocomplete suggestions.
})
)
const jsonSchema = JSONSchema.make(schema)
console.log(JSON.stringify(jsonSchema, null, 2))
/*
Output:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "number",
"description": "a number",
"title": "number",
"minimum": 0,
"maximum": 10
}
*/
For all other types of schema that are not refinements, the content of the annotation is used and overrides anything the system would have generated by default:
import { JSONSchema, Schema } from "@effect/schema"
const schema = Schema.Struct({ foo: Schema.String }).annotations({
jsonSchema: { type: "object" }
})
const jsonSchema = JSONSchema.make(schema)
console.log(JSON.stringify(jsonSchema, null, 2))
/*
Output
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object"
}
the default would be:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": [
"foo"
],
"properties": {
"foo": {
"type": "string",
"description": "a string",
"title": "string"
}
},
"additionalProperties": false
}
*/
Generating Equivalences
The make
function, which is part of the @effect/schema/Equivalence
module, allows you to generate an Equivalence based on a schema definition:
import { Equivalence, Schema } from "@effect/schema"
const Person = Schema.Struct({
name: Schema.String,
age: Schema.Number
})
// $ExpectType Equivalence<{ readonly name: string; readonly age: number; }>
const PersonEquivalence = Equivalence.make(Person)
const john = { name: "John", age: 23 }
const alice = { name: "Alice", age: 30 }
console.log(PersonEquivalence(john, { name: "John", age: 23 })) // Output: true
console.log(PersonEquivalence(john, alice)) // Output: false
Customizations
You can customize the output using the equivalence
annotation:
import { Equivalence, Schema } from "@effect/schema"
const schema = Schema.String.annotations({
equivalence: () => (a, b) => a.at(0) === b.at(0)
})
console.log(Equivalence.make(schema)("aaa", "abb")) // Output: true
Basic Usage
Cheatsheet
Typescript Type | Description / Notes | Schema / Combinator |
---|---|---|
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(S.Literal("a"), S.String) |
{ readonly a: string, readonly b: number } | structs | S.Struct({ a: S.String, b: S.Number }) |
{ readonly a?: string \| undefined } | optional fields | S.Struct({ a: S.optional(S.String) }) |
{ readonly a?: string } | optional fields | S.Struct({ a: S.optional(S.String, { exact: true }) }) |
Record<A, B> | records | S.Record(A, 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(A, B), S.Record(C, D)) |
type A = { readonly 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) |
Primitives
Here are the primitive schemas provided by the @effect/schema/Schema
module:
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>
These primitive schemas are building blocks for creating more complex schemas to describe your data structures.
Literals
Literals in schemas represent specific values that are directly specified. Here are some examples of literal schemas provided by the @effect/schema/Schema
module:
import { Schema } from "@effect/schema"
Schema.Null // same as S.Literal(null)
Schema.Literal("a")
Schema.Literal("a", "b", "c") // union of literals
Schema.Literal(1)
Schema.Literal(2n) // BigInt literal
Schema.Literal(true)
We can also use pickLiteral
with a literal schema to narrow down the possible values:
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:
import { Schema } from "@effect/schema"
const FruitId = Schema.Number
// the source of truth regarding the Fruit category
const FruitCategory = Schema.Literal("sweet", "citrus", "tropical")
const Fruit = Schema.Struct({
id: FruitId,
category: FruitCategory
})
// Here, we want to reuse our FruitCategory definition to create a subtype of Fruit
const SweetAndCitrusFruit = Schema.Struct({
fruitId: FruitId,
category: FruitCategory.pipe(Schema.pickLiteral("sweet", "citrus"))
/*
By using pickLiteral from the FruitCategory, we ensure that the values selected
are 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.
Exposed Values
You can access the literals of a literal schema:
import { Schema } from "@effect/schema"
const schema = Schema.Literal("a", "b")
// Accesses the literals
const literals = schema.literals // readonly ["a", "b"]
Template literals
The TemplateLiteral
constructor allows you to create a schema for a TypeScript template literal type.
import { Schema } from "@effect/schema"
// Schema<`a${string}`>
Schema.TemplateLiteral(Schema.Literal("a"), Schema.String)
// example from https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html
const EmailLocaleIDs = Schema.Literal("welcome_email", "email_heading")
const FooterLocaleIDs = Schema.Literal("footer_title", "footer_sendoff")
// Schema<"welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id">
Schema.TemplateLiteral(
Schema.Union(EmailLocaleIDs, FooterLocaleIDs),
Schema.Literal("_id")
)
Unique Symbols
import { Schema } from "@effect/schema"
const mySymbol = Symbol.for("mysymbol")
// const mySymbolSchema: S.Schema<typeof mySymbol>
const mySymbolSchema = Schema.UniqueSymbolFromSelf(mySymbol)
Filters
In the @effect/schema/Schema
library, you can apply custom validation logic using filters.
You can define a custom validation check on any schema using the filter
function. Here's a simple example:
import { Schema } from "@effect/schema"
const LongString = Schema.String.pipe(
Schema.filter((s) =>
s.length >= 10 ? undefined : "a string at least 10 characters long"
)
)
console.log(Schema.decodeUnknownSync(LongString)("a"))
/*
throws:
Error: { string | filter }
└─ Predicate refinement failure
└─ a string at least 10 characters long
*/
In the new signature of filter
, the type of the predicate passed as an argument is as follows:
predicate: (a: A, options: ParseOptions, self: AST.Refinement) =>
undefined | boolean | string | ParseResult.ParseIssue
with the following semantics:
true
means the filter is successful.false
orundefined
means the filter fails and no default message is set.string
means the filter fails and the returned string is used as the default message.ParseIssue
means the filter fails and the returned ParseIssue is used as an error.
It's also recommended to include as much metadata as possible for later introspection of the schema, such as an identifier, JSON schema representation, and a description:
import { Schema } from "@effect/schema"
const LongString = 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:
Error: LongString
└─ Predicate refinement failure
└─ a string at least 10 characters long
*/
For more complex scenarios, you can return a ParseIssue
. Here's an example:
import { ParseResult, Schema } from "@effect/schema"
const schema = Schema.Struct({ a: Schema.String, b: Schema.String }).pipe(
Schema.filter((o) =>
o.b === o.a
? undefined
: new ParseResult.Type(
Schema.Literal(o.a).ast,
o.b,
`b ("${o.b}") should be equal to a ("${o.a}")`
)
)
)
console.log(Schema.decodeUnknownSync(schema)({ a: "foo", b: "bar" }))
/*
throws:
Error: { { readonly a: string; readonly b: string } | filter }
└─ Predicate refinement failure
└─ b ("bar") should be equal to a ("foo")
*/
!WARNING 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 theType
, consider using Branded types.
Exposed Values
You can access the base schema for which the filter has been defined:
import { Schema } from "@effect/schema"
const LongString = Schema.String.pipe(Schema.filter((s) => s.length >= 10))
// const From: typeof Schema.String
const From = 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
import { Schema } from "@effect/schema"
Schema.String.pipe(Schema.maxLength(5)) // Specifies maximum length of a string
Schema.String.pipe(Schema.minLength(5)) // Specifies minimum length of a string
Schema.NonEmpty // Equivalent to ensuring the string has a minimum length of 1
Schema.String.pipe(Schema.length(5)) // Specifies exact length of a string
Schema.String.pipe(Schema.length({ min: 2, max: 4 })) // Specifies a range for the length of a string
Schema.String.pipe(Schema.pattern(regex)) // Matches a string against a regular expression pattern
Schema.String.pipe(Schema.startsWith(string)) // Ensures a string starts with a specific substring
Schema.String.pipe(Schema.endsWith(string)) // Ensures a string ends with a specific substring
Schema.String.pipe(Schema.includes(searchString)) // Checks if a string includes a specific substring
Schema.String.pipe(Schema.trimmed()) // Validates that a string has no leading or trailing whitespaces
Schema.String.pipe(Schema.lowercased()) // Validates that a string is entirely in lowercase
!NOTE 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 thetrim
combinator ot theTrim
schema.
Number Filters
import { Schema } from "@effect/schema"
Schema.Number.pipe(Schema.greaterThan(5)) // Specifies a number greater than 5
Schema.Number.pipe(Schema.greaterThanOrEqualTo(5)) // Specifies a number greater than or equal to 5
Schema.Number.pipe(Schema.lessThan(5)) // Specifies a number less than 5
Schema.Number.pipe(Schema.lessThanOrEqualTo(5)) // Specifies a number less than or equal to 5
Schema.Number.pipe(Schema.between(-2, 2)) // Specifies a number between -2 and 2, inclusive
Schema.Number.pipe(Schema.int()) // Specifies that the value must be an integer
Schema.Number.pipe(Schema.nonNaN()) // Ensures the value is not NaN
Schema.Number.pipe(Schema.finite()) // Ensures the value is finite and not Infinity or -Infinity
Schema.Number.pipe(Schema.positive()) // Specifies a positive number (> 0)
Schema.Number.pipe(Schema.nonNegative()) // Specifies a non-negative number (>= 0)
Schema.Number.pipe(Schema.negative()) // Specifies a negative number (< 0)
Schema.Number.pipe(Schema.nonPositive()) // Specifies a non-positive number (<= 0)
Schema.Number.pipe(Schema.multipleOf(5)) // Specifies a number that is evenly divisible by 5
BigInt Filters
import { Schema } from "@effect/schema"
Schema.BigInt.pipe(Schema.greaterThanBigInt(5n)) // Specifies a BigInt greater than 5
Schema.BigInt.pipe(Schema.greaterThanOrEqualToBigInt(5n)) // Specifies a BigInt greater than or equal to 5
Schema.BigInt.pipe(Schema.lessThanBigInt(5n)) // Specifies a BigInt less than 5
Schema.BigInt.pipe(Schema.lessThanOrEqualToBigInt(5n)) // Specifies a BigInt less than or equal to 5
Schema.BigInt.pipe(Schema.betweenBigInt(-2n, 2n)) // Specifies a BigInt between -2 and 2, inclusive
Schema.BigInt.pipe(Schema.positiveBigInt()) // Specifies a positive BigInt (> 0n)
Schema.BigInt.pipe(Schema.nonNegativeBigInt()) // Specifies a non-negative BigInt (>= 0n)
Schema.BigInt.pipe(Schema.negativeBigInt()) // Specifies a negative BigInt (< 0n)
Schema.BigInt.pipe(Schema.nonPositiveBigInt()) // Specifies a non-positive BigInt (<= 0n)
BigDecimal Filters
import { Schema } from "@effect/schema"
import { BigDecimal } from "effect"
Schema.BigDecimal.pipe(Schema.greaterThanBigDecimal(BigDecimal.fromNumber(5))) // Specifies a BigDecimal greater than 5
Schema.BigDecimal.pipe(
Schema.greaterThanOrEqualToBigDecimal(BigDecimal.fromNumber(5))
) // Specifies a BigDecimal greater than or equal to 5
Schema.BigDecimal.pipe(Schema.lessThanBigDecimal(BigDecimal.fromNumber(5))) // Specifies a BigDecimal less than 5
Schema.BigDecimal.pipe(
Schema.lessThanOrEqualToBigDecimal(BigDecimal.fromNumber(5))
) // Specifies a BigDecimal less than or equal to 5
Schema.BigDecimal.pipe(
Schema.betweenBigDecimal(BigDecimal.fromNumber(-2), BigDecimal.fromNumber(2))
) // Specifies a BigDecimal between -2 and 2, inclusive
Schema.BigDecimal.pipe(Schema.positiveBigDecimal()) // Specifies a positive BigDecimal (> 0)
Schema.BigDecimal.pipe(Schema.nonNegativeBigDecimal()) // Specifies a non-negative BigDecimal (>= 0)
Schema.BigDecimal.pipe(Schema.negativeBigDecimal()) // Specifies a negative BigDecimal (< 0)
Schema.BigDecimal.pipe(Schema.nonPositiveBigDecimal()) // Specifies a non-positive BigDecimal (<= 0)
Duration Filters
import { Schema } from "@effect/schema"
Schema.Duration.pipe(Schema.greaterThanDuration("5 seconds")) // Specifies a duration greater than 5 seconds
Schema.Duration.pipe(Schema.greaterThanOrEqualToDuration("5 seconds")) // Specifies a duration greater than or equal to 5 seconds
Schema.Duration.pipe(Schema.lessThanDuration("5 seconds")) // Specifies a duration less than 5 seconds
Schema.Duration.pipe(Schema.lessThanOrEqualToDuration("5 seconds")) // Specifies a duration less than or equal to 5 seconds
Schema.Duration.pipe(Schema.betweenDuration("5 seconds", "10 seconds")) // Specifies a duration between 5 seconds and 10 seconds, inclusive
Array Filters
import { Schema } from "@effect/schema"
Schema.Array(Schema.Number).pipe(Schema.maxItems(2)) // Specifies the maximum number of items in the array
Schema.Array(Schema.Number).pipe(Schema.minItems(2)) // Specifies the minimum number of items in the array
Schema.Array(Schema.Number).pipe(Schema.itemsCount(2)) // Specifies the exact number of items in the array
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.
type UserId = string
type Username = string
const getUser = (id: UserId) => { ... }
const myUsername: Username = "gcanti"
getUser(myUsername) // works fine
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".
import { Brand } from "effect"
type UserId = string & Brand.Brand<"UserId">
type Username = string
const getUser = (id: UserId) => { ... }
const myUsername: Username = "gcanti"
getUser(myUsername) // error
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 schema from scratch
To define a schema for a branded type from scratch, you can use the brand
combinator exported by the @effect/schema/Schema
module. Here's an example:
import { Schema } from "@effect/schema"
const UserId = Schema.String.pipe(Schema.brand("UserId"))
type UserId = Schema.Schema.Type<typeof UserId> // string & Brand<"UserId">
Note that you can use unique symbol
s as brands to ensure uniqueness across modules / packages:
import { Schema } from "@effect/schema"
const UserIdBrand = Symbol.for("UserId")
const UserId = Schema.String.pipe(Schema.brand(UserIdBrand))
// string & Brand<typeof UserIdBrand>
type UserId = Schema.Schema.Type<typeof UserId>
23 hours ago
1 day ago
1 day ago
1 day ago
5 days ago
5 days ago
6 days ago
7 days ago
8 days ago
11 days ago
12 days ago
13 days ago
18 days ago
19 days ago
21 days ago
21 days ago
24 days ago
24 days ago
26 days ago
26 days ago
29 days ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
6 months ago
6 months ago
6 months ago
7 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
6 months ago
10 months ago
6 months ago
6 months ago
6 months ago
6 months ago
10 months ago
6 months ago
6 months ago
6 months ago
11 months ago
7 months ago
11 months ago
6 months ago
10 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
9 months ago
10 months ago
10 months ago
6 months ago
7 months ago
6 months ago
7 months ago
7 months ago
10 months ago
6 months ago
10 months ago
7 months ago
7 months ago
7 months ago
11 months ago
6 months ago
7 months ago
6 months ago
7 months ago
8 months ago
8 months ago
8 months ago
9 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
7 months ago
7 months ago
10 months ago
7 months ago
7 months ago
7 months ago
11 months ago
8 months ago
8 months ago
8 months ago
8 months ago
9 months ago
8 months ago
10 months ago
6 months ago
6 months ago
6 months ago
10 months ago
7 months ago
7 months ago
7 months ago
11 months ago
7 months ago
7 months ago
6 months ago
6 months ago
6 months ago
11 months ago
11 months ago
11 months ago
12 months ago
12 months ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago