0.67.9 • Published 23 hours ago

@effect/schema v0.67.9

Weekly downloads
-
License
MIT
Repository
github
Last release
23 hours ago

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:

OperationDescription
DecodingTransforming data from an input type Encoded to an output type Type.
EncodingConverting data from an output type Type back to an input type Encoded.
AssertingVerifying that a value adheres to the schema's output type Type.
ArbitrariesGenerate arbitraries for fast-check testing.
Pretty printingSupport pretty printing for data structures.
JSON SchemasCreate JSON Schemas based on defined schemas.
EquivalenceCreate 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 is never (default if not explicitly provided), it means the schema has no requirements.

Examples

  • Schema<string> (defaulted to Schema<string, string, never>) represents a schema that decodes to string, encodes to string, and has no requirements.
  • Schema<number, string> (defaulted to Schema<number, string, never>) represents a schema that decodes to number from string, encodes a number to a string, and has no requirements.

!NOTE In the Effect ecosystem, you may often encounter the type parameters of Schema abbreviated as A, I, and R 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:

  1. 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 a string.

  2. Decoding: Following the successful check, we proceed to convert the string into a Date. This process completes the decoding operation, where the data is both validated and transformed.

Encoding From Unknown

Encoding from unknown involves two key steps:

  1. 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 a Date.

  2. Encoding: Following the successful check, we proceed to convert the Date into a string. 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 your tsconfig.json file
  • The exactOptionalPropertyTypes flag enabled in your tsconfig.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 return readonly types. For instance, in the Person 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 an Option type.
  • decodeUnknownEither: Decodes a value and returns an Either type.
  • decodeUnknownPromise: Decodes a value and returns a Promise.
  • decodeUnknown: Decodes a value and returns an Effect.

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 and error 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 and error 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 an Option type.
  • encodeEither: Encodes data and returns an Either type representing success or failure.
  • encodePromise: Encodes data and returns a Promise.
  • encode: Encodes data and returns an Effect.

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 and error 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 an Either, where a successful decoding results in a Right and a failed decoding results in a Left containing the original input.

  • Encoding: The encoding process uses the Forbidden error to indicate that encoding a Left value is not supported. Only Right 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 the identifier 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 a satisfies 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 TypeDescription / NotesSchema / Combinator
nullS.Null
undefinedS.Undefined
stringS.String
numberS.Number
booleanS.Boolean
symbolS.SymbolFromSelf / S.Symbol
BigIntS.BigIntFromSelf / S.BigInt
unknownS.Unknown
anyS.Any
neverS.Never
objectS.Object
unique symbolS.UniqueSymbolFromSelf
"a", 1, truetype literalsS.Literal("a"), S.Literal(1), S.Literal(true)
a${string}template literalsS.TemplateLiteral(S.Literal("a"), S.String)
{ readonly a: string, readonly b: number }structsS.Struct({ a: S.String, b: S.Number })
{ readonly a?: string \| undefined }optional fieldsS.Struct({ a: S.optional(S.String) })
{ readonly a?: string }optional fieldsS.Struct({ a: S.optional(S.String, { exact: true }) })
Record<A, B>recordsS.Record(A, B)
readonly [string, number]tuplesS.Tuple(S.String, S.Number)
ReadonlyArray<string>arraysS.Array(S.String)
A \| BunionsS.Union(A, B)
A & Bintersections of non-overlapping structsS.extend(A, B)
Record<A, B> & Record<C, D>intersections of non-overlapping recordsS.extend(S.Record(A, B), S.Record(C, D))
type A = { readonly a: A \| null }recursive typesS.Struct({ a: S.Union(S.Null, S.suspend(() => self)) })
keyof AS.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 or undefined 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 the Type, 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 the trim combinator ot the Trim 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 symbols 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>

@everything-registry/sub-chunk-279effect-db-schemaeffect-drizzleeffect-errorseffect-typeseffect-schema-compilersgit-jira-branchlavalink-protocollavaclientgbtharumomnisintuitajazz-runjazz-toolsmenimalmeow-inquirerpacktorysyncpackochiopenapi-to-effectremote-tarball-fetcherspotify-effectts-to-effect-schemauploadthinguteiusvoluptatemenim@dxos/react-ui-table@dxos/functions@creative-introvert/prediction-testing@creative-introvert/prediction-testing-cli@creative-introvert/tons-of-tests@creative-introvert/tons-of-tests-cli@fr33m0nk/clearbit-client@fr33m0nk/lusha-client@fubhy/docgen@dxos/echo-schema@evmts/viem@evmts/config@evmts/effect@evmts/schemas@evmts/blockexplorer@daotl-effect/prelude@effect-use/gcp-logging@effect-use/github@effect-use/http-client@effect-use/stripe@effect-use/temporal-client@effect-use/temporal-config@effect-app/schema2@effect-app/prelude@effect-use/aws-s3@effect-use/brex@effect-use/gcp-gcs@kumori/cmc@kumori/cmccli@kumori/kam@konker.dev/aws-client-effect-dynamodb@konker.dev/aws-client-effect-s3@konker.dev/funcsmith@konker.dev/node-ts-fp-boilerplate@konker.dev/tiny-event-fp@konker.dev/tiny-filesystem-fp@konker.dev/tiny-fsm-fp@konker.dev/tiny-rules-fp@konker.dev/tiny-treecrawler-fp@kieran-osgood/scribe@intuita-inc/utilities@jmorecroft67/pg-stream-cli@lavaclient/plugin-lavasearch@lindystack/json-schema@livestore/sql-queries@livestore/utils@lucid-evolution/lucid@passlock/shared@rebaze-fr/util-effect-utils-next-schema@rebaze-fr/data-access-pgsql-next@rebaze-fr/util-effect-prisma-next@tevm/viem-effect@typed/dom@typed/fx@typed/guard@typed/route@typed/router@typed/server@typed/storybook@typed/template@typed/ui@typed/async-data@typed/core@typed/decoder@typed/id@typed/navigation@typed/path@tstelzer/perf-metrics-core@typed-at-rest/client@typed-at-rest/core@typed-at-rest/server@tevm/schemas@tevm/config@boostercloud/framework-core
0.67.9

23 hours ago

0.67.6

1 day ago

0.67.8

1 day ago

0.67.7

1 day ago

0.67.4

5 days ago

0.67.5

5 days ago

0.67.3

6 days ago

0.67.2

7 days ago

0.67.1

8 days ago

0.67.0

11 days ago

0.66.16

12 days ago

0.66.15

13 days ago

0.66.14

18 days ago

0.66.13

19 days ago

0.66.12

21 days ago

0.66.11

21 days ago

0.66.9

24 days ago

0.66.10

24 days ago

0.66.7

26 days ago

0.66.8

26 days ago

0.66.6

29 days ago

0.66.5

1 month ago

0.66.4

1 month ago

0.66.3

1 month ago

0.66.2

1 month ago

0.66.1

1 month ago

0.65.0

1 month ago

0.66.0

1 month ago

0.64.20

1 month ago

0.64.19

2 months ago

0.64.17

2 months ago

0.64.18

2 months ago

0.64.16

2 months ago

0.64.15

2 months ago

0.64.14

2 months ago

0.64.13

2 months ago

0.64.12

2 months ago

0.64.11

2 months ago

0.64.10

2 months ago

0.64.9

2 months ago

0.64.8

2 months ago

0.64.7

2 months ago

0.64.6

2 months ago

0.64.5

2 months ago

0.64.4

2 months ago

0.64.3

2 months ago

0.64.2

2 months ago

0.64.1

2 months ago

0.64.0

2 months ago

0.63.4

2 months ago

0.63.3

2 months ago

0.63.2

3 months ago

0.63.1

3 months ago

0.63.0

3 months ago

0.62.9

3 months ago

0.62.8

3 months ago

0.62.7

3 months ago

0.62.6

3 months ago

0.62.3

3 months ago

0.62.2

3 months ago

0.62.5

3 months ago

0.62.4

3 months ago

0.62.1

3 months ago

0.62.0

3 months ago

0.61.6

3 months ago

0.61.7

3 months ago

0.61.5

4 months ago

0.61.4

4 months ago

0.61.2

4 months ago

0.61.1

4 months ago

0.61.3

4 months ago

0.61.0

4 months ago

0.60.7

4 months ago

0.60.6

4 months ago

0.60.5

4 months ago

0.60.4

4 months ago

0.60.3

4 months ago

0.60.2

4 months ago

0.60.1

4 months ago

0.60.0

4 months ago

0.59.1

4 months ago

0.59.0

4 months ago

0.57.2

4 months ago

0.58.0

4 months ago

0.57.1

5 months ago

0.57.0

5 months ago

0.56.1

5 months ago

0.56.0

5 months ago

0.55.0

5 months ago

0.54.1

5 months ago

0.54.0

5 months ago

0.53.3

5 months ago

0.53.2

5 months ago

0.53.1

5 months ago

0.53.0

5 months ago

0.52.0

6 months ago

0.51.5

6 months ago

0.51.4

6 months ago

0.43.0

7 months ago

0.36.4

8 months ago

0.36.3

8 months ago

0.36.2

8 months ago

0.36.1

8 months ago

0.36.0

8 months ago

0.36.5

8 months ago

0.51.2

6 months ago

0.32.0

10 months ago

0.51.3

6 months ago

0.51.0

6 months ago

0.51.1

6 months ago

0.48.2

6 months ago

0.29.0

10 months ago

0.48.3

6 months ago

0.48.0

6 months ago

0.48.1

6 months ago

0.25.0

11 months ago

0.44.0

7 months ago

0.21.1

11 months ago

0.48.4

6 months ago

0.29.1

10 months ago

0.40.2

8 months ago

0.40.0

8 months ago

0.40.1

8 months ago

0.37.2

8 months ago

0.37.1

8 months ago

0.37.0

8 months ago

0.33.2

9 months ago

0.33.1

10 months ago

0.33.0

10 months ago

0.49.1

6 months ago

0.45.5

7 months ago

0.49.2

6 months ago

0.45.6

7 months ago

0.45.3

7 months ago

0.26.1

10 months ago

0.49.0

6 months ago

0.26.0

10 months ago

0.45.1

7 months ago

0.45.2

7 months ago

0.45.0

7 months ago

0.22.0

11 months ago

0.49.3

6 months ago

0.45.7

7 months ago

0.49.4

6 months ago

0.45.8

7 months ago

0.41.1

8 months ago

0.41.0

8 months ago

0.38.0

8 months ago

0.34.0

9 months ago

0.30.4

10 months ago

0.30.3

10 months ago

0.30.2

10 months ago

0.30.1

10 months ago

0.30.0

10 months ago

0.46.4

7 months ago

0.46.2

7 months ago

0.27.0

10 months ago

0.46.3

7 months ago

0.46.0

7 months ago

0.46.1

7 months ago

0.23.0

11 months ago

0.42.0

8 months ago

0.39.1

8 months ago

0.39.0

8 months ago

0.35.1

8 months ago

0.35.0

9 months ago

0.39.2

8 months ago

0.31.0

10 months ago

0.50.0

6 months ago

0.47.3

6 months ago

0.47.4

6 months ago

0.28.0

10 months ago

0.47.1

7 months ago

0.47.2

7 months ago

0.47.0

7 months ago

0.24.0

11 months ago

0.43.1

7 months ago

0.43.2

7 months ago

0.47.7

6 months ago

0.47.5

6 months ago

0.47.6

6 months ago

0.21.0

11 months ago

0.20.3

11 months ago

0.20.2

11 months ago

0.20.1

12 months ago

0.20.0

12 months ago

0.19.3

1 year ago

0.19.2

1 year ago

0.19.1

1 year ago

0.19.0

1 year ago

0.18.0

1 year ago

0.17.5

1 year ago

0.17.4

1 year ago

0.17.3

1 year ago

0.17.2

1 year ago

0.17.1

1 year ago

0.17.0

1 year ago

0.16.0

1 year ago

0.15.0

1 year ago

0.14.1

1 year ago

0.14.0

1 year ago

0.13.1

1 year ago

0.13.0

1 year ago

0.12.1

1 year ago

0.12.0

1 year ago

0.11.1

1 year ago

0.11.0

1 year ago

0.10.0

1 year ago

0.9.1

1 year ago

0.9.0

1 year ago

0.8.3

1 year ago

0.8.2

1 year ago

0.8.1

1 year ago

0.8.0

1 year ago

0.7.1

1 year ago

0.7.0

1 year ago

0.6.0

1 year ago

0.5.0

1 year ago

0.4.0

1 year ago

0.3.1

1 year ago

0.3.0

1 year ago

0.2.1

1 year ago

0.2.0

1 year ago

0.1.0

1 year ago

0.0.5

1 year ago

0.0.4

1 year ago

0.0.3

1 year ago

0.0.2

1 year ago

0.0.1

1 year ago