Basic Usage

On this page

Cheatsheet

Typescript TypeDescription / NotesSchema / Combinator / Example
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("a", S.String)
{ readonly a: string, readonly b?: number| undefined }structsS.Struct({ a: S.String, b: S.optional(S.Number) })
Record<A, B>recordsS.Record({ key: A, value: 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({ key: A, value: B }), S.Record({ key: C, value: D }))
type A = { 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)

Here are the primitive schemas provided by the @effect/schema/Schema module:

Primitives

These primitive schemas are building blocks for creating more complex schemas to describe your data structures.

ts
import { Schema } from "@effect/schema"
 
Schema.String // Schema<string>
Schema.Number // Schema<number>
Schema.Boolean // Schema<boolean>
Schema.BigIntFromSelf // Schema<BigInt>
Schema.SymbolFromSelf // Schema<symbol>
Schema.Object // Schema<object>
Schema.Undefined // Schema<undefined>
Schema.Void // Schema<void>
Schema.Any // Schema<any>
Schema.Unknown // Schema<unknown>
Schema.Never // Schema<never>
ts
import { Schema } from "@effect/schema"
 
Schema.String // Schema<string>
Schema.Number // Schema<number>
Schema.Boolean // Schema<boolean>
Schema.BigIntFromSelf // Schema<BigInt>
Schema.SymbolFromSelf // Schema<symbol>
Schema.Object // Schema<object>
Schema.Undefined // Schema<undefined>
Schema.Void // Schema<void>
Schema.Any // Schema<any>
Schema.Unknown // Schema<unknown>
Schema.Never // Schema<never>

Literals

Literals represent specific values that are directly specified.

ts
import { Schema } from "@effect/schema"
 
Schema.Null // same as S.Literal(null)
Schema.Literal("a")
Schema.Literal("a", "b", "c") // union of literals
Schema.Literal(1)
Schema.Literal(2n) // BigInt literal
Schema.Literal(true)
ts
import { Schema } from "@effect/schema"
 
Schema.Null // same as S.Literal(null)
Schema.Literal("a")
Schema.Literal("a", "b", "c") // union of literals
Schema.Literal(1)
Schema.Literal(2n) // BigInt literal
Schema.Literal(true)

Exposed Values

You can access the literals of a literal schema:

ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Literal("a", "b")
 
// Accesses the literals
const literals = schema.literals
ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Literal("a", "b")
 
// Accesses the literals
const literals = schema.literals

The pickLiteral Utility

We can also use Schema.pickLiteral with a literal schema to narrow down the possible values:

ts
import { Schema } from "@effect/schema"
 
Schema.Literal("a", "b", "c").pipe(Schema.pickLiteral("a", "b")) // same as S.Literal("a", "b")
ts
import { Schema } from "@effect/schema"
 
Schema.Literal("a", "b", "c").pipe(Schema.pickLiteral("a", "b")) // same as S.Literal("a", "b")

Sometimes, we need to reuse a schema literal in other parts of our code. Let's see an example:

ts
import { Schema } from "@effect/schema"
 
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.
*/
})
ts
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.

Template literals

In TypeScript, template literals allow you to embed expressions within string literals. The Schema.TemplateLiteral constructor allows you to create a schema for these template literal types.

Here's how you can use it:

ts
import { Schema } from "@effect/schema"
 
// This creates a TemplateLiteral of type `a${string}`
Schema.TemplateLiteral("a", Schema.String)
 
// This creates a TemplateLiteral of type `https://${string}.com` or `https://${string}.net`
Schema.TemplateLiteral(
"https://",
Schema.String,
".",
Schema.Literal("com", "net")
)
ts
import { Schema } from "@effect/schema"
 
// This creates a TemplateLiteral of type `a${string}`
Schema.TemplateLiteral("a", Schema.String)
 
// This creates a TemplateLiteral of type `https://${string}.com` or `https://${string}.net`
Schema.TemplateLiteral(
"https://",
Schema.String,
".",
Schema.Literal("com", "net")
)

Let's look at a more complex example. Suppose you have two sets of locale IDs for emails and footers:

ts
import { Schema } from "@effect/schema"
 
// example from https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html
const EmailLocaleIDs = Schema.Literal("welcome_email", "email_heading")
const FooterLocaleIDs = Schema.Literal("footer_title", "footer_sendoff")
ts
import { Schema } from "@effect/schema"
 
// 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")

You can use the Schema.TemplateLiteral constructor to create a schema that combines these IDs:

ts
// This creates a TemplateLiteral of type "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"
Schema.TemplateLiteral(Schema.Union(EmailLocaleIDs, FooterLocaleIDs), "_id")
ts
// This creates a TemplateLiteral of type "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"
Schema.TemplateLiteral(Schema.Union(EmailLocaleIDs, FooterLocaleIDs), "_id")

The Schema.TemplateLiteral constructor supports the following types of spans:

  • Schema.String
  • Schema.Number
  • Literals: string | number | boolean | null | bigint. These can be either wrapped by Schema.Literal or used directly
  • Unions of the above types

TemplateLiteralParser

The Schema.TemplateLiteral constructor, while useful as a simple validator, only verifies that an input conforms to a specific string pattern by converting template literal definitions into regular expressions. Similarly, Schema.pattern employs regular expressions directly for the same purpose. Post-validation, both methods require additional manual parsing to convert the validated string into a usable data format.

To address these limitations and eliminate the need for manual post-validation parsing, the new TemplateLiteralParser API has been developed. It not only validates the input format but also automatically parses it into a more structured and type-safe output, specifically into a tuple format.

This new approach enhances developer productivity by reducing boilerplate code and simplifying the process of working with complex string inputs.

Example

ts
import { Schema } from "@effect/schema"
 
// const schema: Schema.Schema<readonly [number, "a", string], `${string}a${string}`, never>
const schema = Schema.TemplateLiteralParser(
Schema.NumberFromString,
"a",
Schema.NonEmptyString
)
 
console.log(Schema.decodeEither(schema)("100afoo"))
// { _id: 'Either', _tag: 'Right', right: [ 100, 'a', 'foo' ] }
 
console.log(Schema.encode(schema)([100, "a", "foo"]))
// { _id: 'Either', _tag: 'Right', right: '100afoo' }
ts
import { Schema } from "@effect/schema"
 
// const schema: Schema.Schema<readonly [number, "a", string], `${string}a${string}`, never>
const schema = Schema.TemplateLiteralParser(
Schema.NumberFromString,
"a",
Schema.NonEmptyString
)
 
console.log(Schema.decodeEither(schema)("100afoo"))
// { _id: 'Either', _tag: 'Right', right: [ 100, 'a', 'foo' ] }
 
console.log(Schema.encode(schema)([100, "a", "foo"]))
// { _id: 'Either', _tag: 'Right', right: '100afoo' }

Unique Symbols

ts
import { Schema } from "@effect/schema"
 
const mySymbol = Symbol.for("mysymbol")
 
// const mySymbolSchema: S.Schema<typeof mySymbol>
const mySymbolSchema = Schema.UniqueSymbolFromSelf(mySymbol)
ts
import { Schema } from "@effect/schema"
 
const mySymbol = Symbol.for("mysymbol")
 
// const mySymbolSchema: S.Schema<typeof mySymbol>
const mySymbolSchema = Schema.UniqueSymbolFromSelf(mySymbol)

Filters

Using the Schema.filter function, developers can define custom validation logic that goes beyond basic type checks, allowing for in-depth control over the data conformity process. This function applies a predicate to data, and if the data fails the predicate's condition, a custom error message can be returned.

For effectful filters, see filterEffect.

Example: Simple Validation

ts
import { Schema } from "@effect/schema"
 
const LongString = Schema.String.pipe(
Schema.filter(
(s) => s.length >= 10 || "a string at least 10 characters long"
)
)
 
// string
type LongString = typeof LongString.Type
 
console.log(Schema.decodeUnknownSync(LongString)("a"))
/*
throws:
ParseError: { string | filter }
└─ Predicate refinement failure
└─ a string at least 10 characters long
*/
ts
import { Schema } from "@effect/schema"
 
const LongString = Schema.String.pipe(
Schema.filter(
(s) => s.length >= 10 || "a string at least 10 characters long"
)
)
 
// string
type LongString = typeof LongString.Type
 
console.log(Schema.decodeUnknownSync(LongString)("a"))
/*
throws:
ParseError: { string | filter }
└─ Predicate refinement failure
└─ a string at least 10 characters long
*/

Please note that the use of filters do not alter the Type of the schema. They only serve to add additional constraints to the parsing process. If you intend to modify the Type, consider using Branded types.

Predicate Function Structure

The predicate for a filter is defined as follows:

ts
type Predicate = (
a: A,
options: ParseOptions,
self: AST.Refinement
) => FilterReturnType
ts
type Predicate = (
a: A,
options: ParseOptions,
self: AST.Refinement
) => FilterReturnType

where

ts
interface FilterIssue {
readonly path: ReadonlyArray<PropertyKey>
readonly issue: string | ParseResult.ParseIssue
}
type FilterOutput =
| undefined
| boolean
| string
| ParseResult.ParseIssue
| FilterIssue
type FilterReturnType = FilterOutput | ReadonlyArray<FilterOutput>
ts
interface FilterIssue {
readonly path: ReadonlyArray<PropertyKey>
readonly issue: string | ParseResult.ParseIssue
}
type FilterOutput =
| undefined
| boolean
| string
| ParseResult.ParseIssue
| FilterIssue
type FilterReturnType = FilterOutput | ReadonlyArray<FilterOutput>

Filter predicates can return several types of values, each with specific implications:

  • true: The data satisfies the filter's condition.
  • false or undefined: The filter is not satisfied, and no specific error message is provided.
  • string: The filter fails, and the provided string is used as the default error message.
  • ParseResult.ParseIssue: The filter fails with a detailed error structure.
  • FilterIssue: Allows specifying detailed error paths and messages, enhancing error specificity.

An array can be returned if multiple issues need to be reported, allowing for complex validations that may have multiple points of failure.

Annotations

It's beneficial to embed as much metadata as possible within the schema. This metadata can include identifiers, JSON schema specifications, and descriptive text to facilitate later analysis and understanding of the schema's purpose and constraints.

Example

ts
import { Schema } from "@effect/schema"
 
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:
ParseError: { string | filter }
└─ Predicate refinement failure
└─ a string at least 10 characters long
*/
ts
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:
ParseError: { string | filter }
└─ Predicate refinement failure
└─ a string at least 10 characters long
*/

Specifying Error Paths

It's possible to specify an error path along with the message, which enhances error specificity and is particularly beneficial for integration with tools like react-hook-form.

Example

ts
import { ArrayFormatter, Schema } from "@effect/schema"
import { Either } from "effect"
 
const Password = Schema.Trim.pipe(Schema.minLength(1))
 
const MyForm = Schema.Struct({
password: Password,
confirm_password: Password
}).pipe(
Schema.filter((input) => {
if (input.password !== input.confirm_password) {
return {
path: ["confirm_password"],
message: "Passwords do not match"
}
}
})
)
 
console.log(
JSON.stringify(
Schema.decodeUnknownEither(MyForm)({
password: "abc",
confirm_password: "d"
}).pipe(Either.mapLeft((error) => ArrayFormatter.formatErrorSync(error))),
null,
2
)
)
/*
"_id": "Either",
"_tag": "Left",
"left": [
{
"_tag": "Type",
"path": [
"confirm_password"
],
"message": "Passwords do not match"
}
]
}
*/
ts
import { ArrayFormatter, Schema } from "@effect/schema"
import { Either } from "effect"
 
const Password = Schema.Trim.pipe(Schema.minLength(1))
 
const MyForm = Schema.Struct({
password: Password,
confirm_password: Password
}).pipe(
Schema.filter((input) => {
if (input.password !== input.confirm_password) {
return {
path: ["confirm_password"],
message: "Passwords do not match"
}
}
})
)
 
console.log(
JSON.stringify(
Schema.decodeUnknownEither(MyForm)({
password: "abc",
confirm_password: "d"
}).pipe(Either.mapLeft((error) => ArrayFormatter.formatErrorSync(error))),
null,
2
)
)
/*
"_id": "Either",
"_tag": "Left",
"left": [
{
"_tag": "Type",
"path": [
"confirm_password"
],
"message": "Passwords do not match"
}
]
}
*/

This allows the error to be directly associated with the confirm_password field, improving clarity for the end-user.

The use of ArrayFormatter translates the error details into a more comprehensible format. For further details, see ArrayFormatter.

Multiple Error Reporting

The Schema.filter API also supports reporting multiple issues at once, which is useful in forms where several validation checks might fail simultaneously.

Example

ts
import { ArrayFormatter, Schema } from "@effect/schema"
import { Either } from "effect"
 
const Password = Schema.Trim.pipe(Schema.minLength(1))
const OptionalString = Schema.optional(Schema.String)
 
const MyForm = Schema.Struct({
password: Password,
confirm_password: Password,
name: OptionalString,
surname: OptionalString
}).pipe(
Schema.filter((input) => {
const issues: Array<Schema.FilterIssue> = []
// passwords must match
if (input.password !== input.confirm_password) {
issues.push({
path: ["confirm_password"],
message: "Passwords do not match"
})
}
// either name or surname must be present
if (!input.name && !input.surname) {
issues.push({
path: ["surname"],
message: "Surname must be present if name is not present"
})
}
return issues
})
)
 
console.log(
JSON.stringify(
Schema.decodeUnknownEither(MyForm)({
password: "abc",
confirm_password: "d"
}).pipe(Either.mapLeft((error) => ArrayFormatter.formatErrorSync(error))),
null,
2
)
)
/*
{
"_id": "Either",
"_tag": "Left",
"left": [
{
"_tag": "Type",
"path": [
"confirm_password"
],
"message": "Passwords do not match"
},
{
"_tag": "Type",
"path": [
"surname"
],
"message": "Surname must be present if name is not present"
}
]
}
*/
ts
import { ArrayFormatter, Schema } from "@effect/schema"
import { Either } from "effect"
 
const Password = Schema.Trim.pipe(Schema.minLength(1))
const OptionalString = Schema.optional(Schema.String)
 
const MyForm = Schema.Struct({
password: Password,
confirm_password: Password,
name: OptionalString,
surname: OptionalString
}).pipe(
Schema.filter((input) => {
const issues: Array<Schema.FilterIssue> = []
// passwords must match
if (input.password !== input.confirm_password) {
issues.push({
path: ["confirm_password"],
message: "Passwords do not match"
})
}
// either name or surname must be present
if (!input.name && !input.surname) {
issues.push({
path: ["surname"],
message: "Surname must be present if name is not present"
})
}
return issues
})
)
 
console.log(
JSON.stringify(
Schema.decodeUnknownEither(MyForm)({
password: "abc",
confirm_password: "d"
}).pipe(Either.mapLeft((error) => ArrayFormatter.formatErrorSync(error))),
null,
2
)
)
/*
{
"_id": "Either",
"_tag": "Left",
"left": [
{
"_tag": "Type",
"path": [
"confirm_password"
],
"message": "Passwords do not match"
},
{
"_tag": "Type",
"path": [
"surname"
],
"message": "Surname must be present if name is not present"
}
]
}
*/

The use of ArrayFormatter translates the error details into a more comprehensible format. For further details, see ArrayFormatter.

Exposed Values

You can access the base schema for which the filter has been defined:

ts
import { Schema } from "@effect/schema"
 
const LongString = Schema.String.pipe(Schema.filter((s) => s.length >= 10))
 
// const From: typeof Schema.String
const From = LongString.from
ts
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

ts
import { Schema } from "@effect/schema"
 
// Specifies maximum length of a string
Schema.String.pipe(Schema.maxLength(5))
 
// Specifies minimum length of a string
Schema.String.pipe(Schema.minLength(5))
 
// Equivalent to ensuring the string has a minimum length of 1
Schema.NonEmptyString
 
// Specifies exact length of a string
Schema.String.pipe(Schema.length(5))
 
// Specifies a range for the length of a string
Schema.String.pipe(Schema.length({ min: 2, max: 4 }))
 
// Matches a string against a regular expression pattern
Schema.String.pipe(Schema.pattern(/^[a-z]+$/))
 
// Ensures a string starts with a specific substring
Schema.String.pipe(Schema.startsWith("prefix"))
 
// Ensures a string ends with a specific substring
Schema.String.pipe(Schema.endsWith("suffix"))
 
// Checks if a string includes a specific substring
Schema.String.pipe(Schema.includes("substring"))
 
// Validates that a string has no leading or trailing whitespaces
Schema.String.pipe(Schema.trimmed())
 
// Validates that a string is entirely in lowercase
Schema.String.pipe(Schema.lowercased())
ts
import { Schema } from "@effect/schema"
 
// Specifies maximum length of a string
Schema.String.pipe(Schema.maxLength(5))
 
// Specifies minimum length of a string
Schema.String.pipe(Schema.minLength(5))
 
// Equivalent to ensuring the string has a minimum length of 1
Schema.NonEmptyString
 
// Specifies exact length of a string
Schema.String.pipe(Schema.length(5))
 
// Specifies a range for the length of a string
Schema.String.pipe(Schema.length({ min: 2, max: 4 }))
 
// Matches a string against a regular expression pattern
Schema.String.pipe(Schema.pattern(/^[a-z]+$/))
 
// Ensures a string starts with a specific substring
Schema.String.pipe(Schema.startsWith("prefix"))
 
// Ensures a string ends with a specific substring
Schema.String.pipe(Schema.endsWith("suffix"))
 
// Checks if a string includes a specific substring
Schema.String.pipe(Schema.includes("substring"))
 
// Validates that a string has no leading or trailing whitespaces
Schema.String.pipe(Schema.trimmed())
 
// Validates that a string is entirely in lowercase
Schema.String.pipe(Schema.lowercased())

The trimmed combinator does not make any transformations, it only validates. If what you were looking for was a combinator to trim strings, then check out the trim combinator or the Trim schema.

Number Filters

ts
import { Schema } from "@effect/schema"
 
// Specifies a number greater than 5
Schema.Number.pipe(Schema.greaterThan(5))
 
// Specifies a number greater than or equal to 5
Schema.Number.pipe(Schema.greaterThanOrEqualTo(5))
 
// Specifies a number less than 5
Schema.Number.pipe(Schema.lessThan(5))
 
// Specifies a number less than or equal to 5
Schema.Number.pipe(Schema.lessThanOrEqualTo(5))
 
// Specifies a number between -2 and 2, inclusive
Schema.Number.pipe(Schema.between(-2, 2))
 
// Specifies that the value must be an integer
Schema.Number.pipe(Schema.int())
 
// Ensures the value is not NaN
Schema.Number.pipe(Schema.nonNaN())
 
// Ensures the value is finite and not Infinity or -Infinity
Schema.Number.pipe(Schema.finite())
 
// Specifies a positive number (> 0)
Schema.Number.pipe(Schema.positive())
 
// Specifies a non-negative number (>= 0)
Schema.Number.pipe(Schema.nonNegative())
 
// Specifies a negative number (< 0)
Schema.Number.pipe(Schema.negative())
 
// Specifies a non-positive number (<= 0)
Schema.Number.pipe(Schema.nonPositive())
 
// Specifies a number that is evenly divisible by 5
Schema.Number.pipe(Schema.multipleOf(5))
ts
import { Schema } from "@effect/schema"
 
// Specifies a number greater than 5
Schema.Number.pipe(Schema.greaterThan(5))
 
// Specifies a number greater than or equal to 5
Schema.Number.pipe(Schema.greaterThanOrEqualTo(5))
 
// Specifies a number less than 5
Schema.Number.pipe(Schema.lessThan(5))
 
// Specifies a number less than or equal to 5
Schema.Number.pipe(Schema.lessThanOrEqualTo(5))
 
// Specifies a number between -2 and 2, inclusive
Schema.Number.pipe(Schema.between(-2, 2))
 
// Specifies that the value must be an integer
Schema.Number.pipe(Schema.int())
 
// Ensures the value is not NaN
Schema.Number.pipe(Schema.nonNaN())
 
// Ensures the value is finite and not Infinity or -Infinity
Schema.Number.pipe(Schema.finite())
 
// Specifies a positive number (> 0)
Schema.Number.pipe(Schema.positive())
 
// Specifies a non-negative number (>= 0)
Schema.Number.pipe(Schema.nonNegative())
 
// Specifies a negative number (< 0)
Schema.Number.pipe(Schema.negative())
 
// Specifies a non-positive number (<= 0)
Schema.Number.pipe(Schema.nonPositive())
 
// Specifies a number that is evenly divisible by 5
Schema.Number.pipe(Schema.multipleOf(5))

BigInt Filters

ts
import { Schema } from "@effect/schema"
 
// Specifies a BigInt greater than 5
Schema.BigInt.pipe(Schema.greaterThanBigInt(5n))
 
// Specifies a BigInt greater than or equal to 5
Schema.BigInt.pipe(Schema.greaterThanOrEqualToBigInt(5n))
 
// Specifies a BigInt less than 5
Schema.BigInt.pipe(Schema.lessThanBigInt(5n))
 
// Specifies a BigInt less than or equal to 5
Schema.BigInt.pipe(Schema.lessThanOrEqualToBigInt(5n))
 
// Specifies a BigInt between -2n and 2n, inclusive
Schema.BigInt.pipe(Schema.betweenBigInt(-2n, 2n))
 
// Specifies a positive BigInt (> 0n)
Schema.BigInt.pipe(Schema.positiveBigInt())
 
// Specifies a non-negative BigInt (>= 0n)
Schema.BigInt.pipe(Schema.nonNegativeBigInt())
 
// Specifies a negative BigInt (< 0n)
Schema.BigInt.pipe(Schema.negativeBigInt())
 
// Specifies a non-positive BigInt (<= 0n)
Schema.BigInt.pipe(Schema.nonPositiveBigInt())
ts
import { Schema } from "@effect/schema"
 
// Specifies a BigInt greater than 5
Schema.BigInt.pipe(Schema.greaterThanBigInt(5n))
 
// Specifies a BigInt greater than or equal to 5
Schema.BigInt.pipe(Schema.greaterThanOrEqualToBigInt(5n))
 
// Specifies a BigInt less than 5
Schema.BigInt.pipe(Schema.lessThanBigInt(5n))
 
// Specifies a BigInt less than or equal to 5
Schema.BigInt.pipe(Schema.lessThanOrEqualToBigInt(5n))
 
// Specifies a BigInt between -2n and 2n, inclusive
Schema.BigInt.pipe(Schema.betweenBigInt(-2n, 2n))
 
// Specifies a positive BigInt (> 0n)
Schema.BigInt.pipe(Schema.positiveBigInt())
 
// Specifies a non-negative BigInt (>= 0n)
Schema.BigInt.pipe(Schema.nonNegativeBigInt())
 
// Specifies a negative BigInt (< 0n)
Schema.BigInt.pipe(Schema.negativeBigInt())
 
// Specifies a non-positive BigInt (<= 0n)
Schema.BigInt.pipe(Schema.nonPositiveBigInt())

BigDecimal Filters

ts
import { Schema } from "@effect/schema"
import { BigDecimal } from "effect"
 
// Specifies a BigDecimal greater than 5
Schema.BigDecimal.pipe(Schema.greaterThanBigDecimal(BigDecimal.fromNumber(5)))
 
// Specifies a BigDecimal greater than or equal to 5
Schema.BigDecimal.pipe(
Schema.greaterThanOrEqualToBigDecimal(BigDecimal.fromNumber(5))
)
// Specifies a BigDecimal less than 5
Schema.BigDecimal.pipe(Schema.lessThanBigDecimal(BigDecimal.fromNumber(5)))
 
// Specifies a BigDecimal less than or equal to 5
Schema.BigDecimal.pipe(
Schema.lessThanOrEqualToBigDecimal(BigDecimal.fromNumber(5))
)
 
// Specifies a BigDecimal between -2 and 2, inclusive
Schema.BigDecimal.pipe(
Schema.betweenBigDecimal(
BigDecimal.fromNumber(-2),
BigDecimal.fromNumber(2)
)
)
 
// Specifies a positive BigDecimal (> 0)
Schema.BigDecimal.pipe(Schema.positiveBigDecimal())
 
// Specifies a non-negative BigDecimal (>= 0)
Schema.BigDecimal.pipe(Schema.nonNegativeBigDecimal())
 
// Specifies a negative BigDecimal (< 0)
Schema.BigDecimal.pipe(Schema.negativeBigDecimal())
 
// Specifies a non-positive BigDecimal (<= 0)
Schema.BigDecimal.pipe(Schema.nonPositiveBigDecimal())
ts
import { Schema } from "@effect/schema"
import { BigDecimal } from "effect"
 
// Specifies a BigDecimal greater than 5
Schema.BigDecimal.pipe(Schema.greaterThanBigDecimal(BigDecimal.fromNumber(5)))
 
// Specifies a BigDecimal greater than or equal to 5
Schema.BigDecimal.pipe(
Schema.greaterThanOrEqualToBigDecimal(BigDecimal.fromNumber(5))
)
// Specifies a BigDecimal less than 5
Schema.BigDecimal.pipe(Schema.lessThanBigDecimal(BigDecimal.fromNumber(5)))
 
// Specifies a BigDecimal less than or equal to 5
Schema.BigDecimal.pipe(
Schema.lessThanOrEqualToBigDecimal(BigDecimal.fromNumber(5))
)
 
// Specifies a BigDecimal between -2 and 2, inclusive
Schema.BigDecimal.pipe(
Schema.betweenBigDecimal(
BigDecimal.fromNumber(-2),
BigDecimal.fromNumber(2)
)
)
 
// Specifies a positive BigDecimal (> 0)
Schema.BigDecimal.pipe(Schema.positiveBigDecimal())
 
// Specifies a non-negative BigDecimal (>= 0)
Schema.BigDecimal.pipe(Schema.nonNegativeBigDecimal())
 
// Specifies a negative BigDecimal (< 0)
Schema.BigDecimal.pipe(Schema.negativeBigDecimal())
 
// Specifies a non-positive BigDecimal (<= 0)
Schema.BigDecimal.pipe(Schema.nonPositiveBigDecimal())

Duration Filters

ts
import { Schema } from "@effect/schema"
 
// Specifies a duration greater than 5 seconds
Schema.Duration.pipe(Schema.greaterThanDuration("5 seconds"))
 
// Specifies a duration greater than or equal to 5 seconds
Schema.Duration.pipe(Schema.greaterThanOrEqualToDuration("5 seconds"))
 
// Specifies a duration less than 5 seconds
Schema.Duration.pipe(Schema.lessThanDuration("5 seconds"))
 
// Specifies a duration less than or equal to 5 seconds
Schema.Duration.pipe(Schema.lessThanOrEqualToDuration("5 seconds"))
 
// Specifies a duration between 5 seconds and 10 seconds, inclusive
Schema.Duration.pipe(Schema.betweenDuration("5 seconds", "10 seconds"))
ts
import { Schema } from "@effect/schema"
 
// Specifies a duration greater than 5 seconds
Schema.Duration.pipe(Schema.greaterThanDuration("5 seconds"))
 
// Specifies a duration greater than or equal to 5 seconds
Schema.Duration.pipe(Schema.greaterThanOrEqualToDuration("5 seconds"))
 
// Specifies a duration less than 5 seconds
Schema.Duration.pipe(Schema.lessThanDuration("5 seconds"))
 
// Specifies a duration less than or equal to 5 seconds
Schema.Duration.pipe(Schema.lessThanOrEqualToDuration("5 seconds"))
 
// Specifies a duration between 5 seconds and 10 seconds, inclusive
Schema.Duration.pipe(Schema.betweenDuration("5 seconds", "10 seconds"))

Array Filters

ts
import { Schema } from "@effect/schema"
 
// Specifies the maximum number of items in the array
Schema.Array(Schema.Number).pipe(Schema.maxItems(2))
 
// Specifies the minimum number of items in the array
Schema.Array(Schema.Number).pipe(Schema.minItems(2))
 
// Specifies the exact number of items in the array
Schema.Array(Schema.Number).pipe(Schema.itemsCount(2))
ts
import { Schema } from "@effect/schema"
 
// Specifies the maximum number of items in the array
Schema.Array(Schema.Number).pipe(Schema.maxItems(2))
 
// Specifies the minimum number of items in the array
Schema.Array(Schema.Number).pipe(Schema.minItems(2))
 
// Specifies the exact number of items in the array
Schema.Array(Schema.Number).pipe(Schema.itemsCount(2))

Branded types

TypeScript's type system is structural, which means that any two types that are structurally equivalent are considered the same. This can cause issues when types that are semantically different are treated as if they were the same.

ts
type UserId = string
type Username = string
 
declare const getUser: (id: UserId) => object
 
const myUsername: Username = "gcanti"
 
getUser(myUsername) // This erroneously works
ts
type UserId = string
type Username = string
 
declare const getUser: (id: UserId) => object
 
const myUsername: Username = "gcanti"
 
getUser(myUsername) // This erroneously works

In the above example, UserId and Username are both aliases for the same type, string. This means that the getUser function can mistakenly accept a Username as a valid UserId, causing bugs and errors.

To avoid these kinds of issues, the Effect ecosystem provides a way to create custom types with a unique identifier attached to them. These are known as branded types.

ts
import { Brand } from "effect"
 
type UserId = string & Brand.Brand<"UserId">
type Username = string
 
declare const getUser: (id: UserId) => object
 
const myUsername: Username = "gcanti"
 
getUser(myUsername)
Argument of type 'string' is not assignable to parameter of type 'UserId'. Type 'string' is not assignable to type 'Brand<"UserId">'.2345Argument of type 'string' is not assignable to parameter of type 'UserId'. Type 'string' is not assignable to type 'Brand<"UserId">'.
ts
import { Brand } from "effect"
 
type UserId = string & Brand.Brand<"UserId">
type Username = string
 
declare const getUser: (id: UserId) => object
 
const myUsername: Username = "gcanti"
 
getUser(myUsername)
Argument of type 'string' is not assignable to parameter of type 'UserId'. Type 'string' is not assignable to type 'Brand<"UserId">'.2345Argument of type 'string' is not assignable to parameter of type 'UserId'. Type 'string' is not assignable to type 'Brand<"UserId">'.

By defining UserId as a branded type, the getUser function can accept only values of type UserId, and not plain strings or other types that are compatible with strings. This helps to prevent bugs caused by accidentally passing the wrong type of value to the function.

There are two ways to define a schema for a branded type, depending on whether you:

  • want to define the schema from scratch
  • have already defined a branded type via effect/Brand and want to reuse it to define a schema

Defining a brand schema from scratch

To define a schema for a branded type from scratch, you can use the Schema.brand function.

ts
import { Schema } from "@effect/schema"
 
const UserId = Schema.String.pipe(Schema.brand("UserId"))
 
// string & Brand<"UserId">
type UserId = Schema.Schema.Type<typeof UserId>
ts
import { Schema } from "@effect/schema"
 
const UserId = Schema.String.pipe(Schema.brand("UserId"))
 
// string & Brand<"UserId">
type UserId = Schema.Schema.Type<typeof UserId>

Note that you can use unique symbols as brands to ensure uniqueness across modules / packages:

ts
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>
ts
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>

Reusing an existing branded constructor

If you have already defined a branded type using the effect/Brand module, you can reuse it to define a schema using the fromBrand combinator exported by the @effect/schema/Schema module.

ts
import { Schema } from "@effect/schema"
import { Brand } from "effect"
 
// the existing branded type
type UserId = string & Brand.Brand<"UserId">
 
const UserId = Brand.nominal<UserId>()
 
// Define a schema for the branded type
const UserIdSchema = Schema.String.pipe(Schema.fromBrand(UserId))
ts
import { Schema } from "@effect/schema"
import { Brand } from "effect"
 
// the existing branded type
type UserId = string & Brand.Brand<"UserId">
 
const UserId = Brand.nominal<UserId>()
 
// Define a schema for the branded type
const UserIdSchema = Schema.String.pipe(Schema.fromBrand(UserId))

Utilizing Default Constructors

The Schema.brand function includes a default constructor to facilitate the creation of branded values.

ts
import { Schema } from "@effect/schema"
 
const UserId = Schema.String.pipe(Schema.brand("UserId"))
 
const userId = UserId.make("123") // Creates a branded UserId
ts
import { Schema } from "@effect/schema"
 
const UserId = Schema.String.pipe(Schema.brand("UserId"))
 
const userId = UserId.make("123") // Creates a branded UserId

Native enums

ts
import { Schema } from "@effect/schema"
 
enum Fruits {
Apple,
Banana
}
 
// Schema.Enums<typeof Fruits>
const schema = Schema.Enums(Fruits)
ts
import { Schema } from "@effect/schema"
 
enum Fruits {
Apple,
Banana
}
 
// Schema.Enums<typeof Fruits>
const schema = Schema.Enums(Fruits)

Accessing Enum Members

Enums are exposed under an enums property of the schema:

ts
// Access the enum members
schema.enums // Returns all enum members
schema.enums.Apple // Access the Apple member
schema.enums.Banana // Access the Banana member
ts
// Access the enum members
schema.enums // Returns all enum members
schema.enums.Apple // Access the Apple member
schema.enums.Banana // Access the Banana member

Unions

The Schema module includes a built-in Union constructor for composing "OR" types.

ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Union(Schema.String, Schema.Number)
ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Union(Schema.String, Schema.Number)

Union of Literals

While the following is perfectly acceptable:

ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Union(
Schema.Literal("a"),
Schema.Literal("b"),
Schema.Literal("c")
)
ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Union(
Schema.Literal("a"),
Schema.Literal("b"),
Schema.Literal("c")
)

It is possible to use Literal and pass multiple literals, which is less cumbersome:

ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Literal("a", "b", "c")
ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Literal("a", "b", "c")

Nullables

ts
import { Schema } from "@effect/schema"
 
// Represents a schema for a string or null value
Schema.NullOr(Schema.String)
 
// Represents a schema for a string, null, or undefined value
Schema.NullishOr(Schema.String)
 
// Represents a schema for a string or undefined value
Schema.UndefinedOr(Schema.String)
ts
import { Schema } from "@effect/schema"
 
// Represents a schema for a string or null value
Schema.NullOr(Schema.String)
 
// Represents a schema for a string, null, or undefined value
Schema.NullishOr(Schema.String)
 
// Represents a schema for a string or undefined value
Schema.UndefinedOr(Schema.String)

Discriminated unions

TypeScript reference: https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions

Discriminated unions in TypeScript are a way of modeling complex data structures that may take on different forms based on a specific set of conditions or properties. They allow you to define a type that represents multiple related shapes, where each shape is uniquely identified by a shared discriminant property.

In a discriminated union, each variant of the union has a common property, called the discriminant. The discriminant is a literal type, which means it can only have a finite set of possible values. Based on the value of the discriminant property, TypeScript can infer which variant of the union is currently in use.

Here is an example of a discriminated union in TypeScript:

ts
type Circle = {
readonly kind: "circle"
readonly radius: number
}
 
type Square = {
readonly kind: "square"
readonly sideLength: number
}
 
type Shape = Circle | Square
ts
type Circle = {
readonly kind: "circle"
readonly radius: number
}
 
type Square = {
readonly kind: "square"
readonly sideLength: number
}
 
type Shape = Circle | Square

This code defines a discriminated union using the Schema module:

ts
import { Schema } from "@effect/schema"
 
const Circle = Schema.Struct({
kind: Schema.Literal("circle"),
radius: Schema.Number
})
 
const Square = Schema.Struct({
kind: Schema.Literal("square"),
sideLength: Schema.Number
})
 
const Shape = Schema.Union(Circle, Square)
ts
import { Schema } from "@effect/schema"
 
const Circle = Schema.Struct({
kind: Schema.Literal("circle"),
radius: Schema.Number
})
 
const Square = Schema.Struct({
kind: Schema.Literal("square"),
sideLength: Schema.Number
})
 
const Shape = Schema.Union(Circle, Square)

The Literal constructor is used to define the discriminant property with a specific string literal value.

Two structs are defined for Circle and Square, each with their own properties. These structs represent the variants of the union.

Finally, the Union constructor is used to create a schema for the discriminated union Shape, which is a union of Circle and Square.

How to transform a simple union into a discriminated union

If you're working on a TypeScript project and you've defined a simple union to represent a particular input, you may find yourself in a situation where you're not entirely happy with how it's set up. For example, let's say you've defined a Shape union as a combination of Circle and Square without any special property:

ts
import { Schema } from "@effect/schema"
 
const Circle = Schema.Struct({
radius: Schema.Number
})
 
const Square = Schema.Struct({
sideLength: Schema.Number
})
 
const Shape = Schema.Union(Circle, Square)
ts
import { Schema } from "@effect/schema"
 
const Circle = Schema.Struct({
radius: Schema.Number
})
 
const Square = Schema.Struct({
sideLength: Schema.Number
})
 
const Shape = Schema.Union(Circle, Square)

To make your code more manageable, you may want to transform the simple union into a discriminated union. This way, TypeScript will be able to automatically determine which member of the union you're working with based on the value of a specific property.

To achieve this, you can add a special property to each member of the union, which will allow TypeScript to know which type it's dealing with at runtime. Here's how you can transform the Shape schema into another schema that represents a discriminated union:

ts
import { Schema } from "@effect/schema"
 
const Circle = Schema.Struct({
radius: Schema.Number
})
 
const Square = Schema.Struct({
sideLength: Schema.Number
})
 
const DiscriminatedShape = Schema.Union(
Schema.transform(
Circle,
Schema.Struct({ ...Circle.fields, kind: Schema.Literal("circle") }), // Add a "kind" property with the literal value "circle" to Circle
{
strict: true,
decode: (circle) => ({ ...circle, kind: "circle" as const }), // Add the discriminant property to Circle
encode: ({ kind: _kind, ...rest }) => rest // Remove the discriminant property
}
),
 
Schema.transform(
Square,
Schema.Struct({ ...Square.fields, kind: Schema.Literal("square") }), // Add a "kind" property with the literal value "square" to Square
{
strict: true,
decode: (square) => ({ ...square, kind: "square" as const }), // Add the discriminant property to Square
encode: ({ kind: _kind, ...rest }) => rest // Remove the discriminant property
}
)
)
 
console.log(Schema.decodeUnknownSync(DiscriminatedShape)({ radius: 10 }))
// Output: { kind: 'circle', radius: 10 }
 
console.log(Schema.decodeUnknownSync(DiscriminatedShape)({ sideLength: 10 }))
// Output: { kind: 'square', sideLength: 10 }
ts
import { Schema } from "@effect/schema"
 
const Circle = Schema.Struct({
radius: Schema.Number
})
 
const Square = Schema.Struct({
sideLength: Schema.Number
})
 
const DiscriminatedShape = Schema.Union(
Schema.transform(
Circle,
Schema.Struct({ ...Circle.fields, kind: Schema.Literal("circle") }), // Add a "kind" property with the literal value "circle" to Circle
{
strict: true,
decode: (circle) => ({ ...circle, kind: "circle" as const }), // Add the discriminant property to Circle
encode: ({ kind: _kind, ...rest }) => rest // Remove the discriminant property
}
),
 
Schema.transform(
Square,
Schema.Struct({ ...Square.fields, kind: Schema.Literal("square") }), // Add a "kind" property with the literal value "square" to Square
{
strict: true,
decode: (square) => ({ ...square, kind: "square" as const }), // Add the discriminant property to Square
encode: ({ kind: _kind, ...rest }) => rest // Remove the discriminant property
}
)
)
 
console.log(Schema.decodeUnknownSync(DiscriminatedShape)({ radius: 10 }))
// Output: { kind: 'circle', radius: 10 }
 
console.log(Schema.decodeUnknownSync(DiscriminatedShape)({ sideLength: 10 }))
// Output: { kind: 'square', sideLength: 10 }

The previous solution works perfectly and shows how we can add properties to our schema at will, making it easier to consume the result within our domain model. However, it requires a lot of boilerplate. Fortunately, there is an API called Schema.attachPropertySignature designed specifically for this use case, which allows us to achieve the same result with much less effort:

ts
import { Schema } from "@effect/schema"
 
const Circle = Schema.Struct({ radius: Schema.Number })
const Square = Schema.Struct({ sideLength: Schema.Number })
const DiscriminatedShape = Schema.Union(
Circle.pipe(Schema.attachPropertySignature("kind", "circle")),
Square.pipe(Schema.attachPropertySignature("kind", "square"))
)
 
// decoding
console.log(Schema.decodeUnknownSync(DiscriminatedShape)({ radius: 10 }))
// Output: { kind: 'circle', radius: 10 }
 
// encoding
console.log(
Schema.encodeSync(DiscriminatedShape)({
kind: "circle",
radius: 10
})
)
// Output: { radius: 10 }
ts
import { Schema } from "@effect/schema"
 
const Circle = Schema.Struct({ radius: Schema.Number })
const Square = Schema.Struct({ sideLength: Schema.Number })
const DiscriminatedShape = Schema.Union(
Circle.pipe(Schema.attachPropertySignature("kind", "circle")),
Square.pipe(Schema.attachPropertySignature("kind", "square"))
)
 
// decoding
console.log(Schema.decodeUnknownSync(DiscriminatedShape)({ radius: 10 }))
// Output: { kind: 'circle', radius: 10 }
 
// encoding
console.log(
Schema.encodeSync(DiscriminatedShape)({
kind: "circle",
radius: 10
})
)
// Output: { radius: 10 }

Please note that with Schema.attachPropertySignature, you can only add a property, it cannot override an existing one.

Exposed Values

You can access the individual members of a union schema represented as a tuple:

ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Union(Schema.String, Schema.Number)
 
// Accesses the members of the union
const members = schema.members
ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Union(Schema.String, Schema.Number)
 
// Accesses the members of the union
const members = schema.members

Tuples

Required Elements

To define a tuple with required elements, you specify the list of elements:

ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Tuple(Schema.String, Schema.Number)
ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Tuple(Schema.String, Schema.Number)

Append a Required Element

ts
import { Schema } from "@effect/schema"
 
const tuple1 = Schema.Tuple(Schema.String, Schema.Number)
 
const tuple2 = Schema.Tuple(...tuple1.elements, Schema.Boolean)
ts
import { Schema } from "@effect/schema"
 
const tuple1 = Schema.Tuple(Schema.String, Schema.Number)
 
const tuple2 = Schema.Tuple(...tuple1.elements, Schema.Boolean)

Optional Elements

To define an optional element, wrap the schema of the element with the optionalElement constructor:

ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Tuple(
Schema.String, // required element
Schema.optionalElement(Schema.Number) // optional element
)
ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Tuple(
Schema.String, // required element
Schema.optionalElement(Schema.Number) // optional element
)

Rest Element

To define rest elements, follow the list of elements (required or optional) with an element for the rest:

ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Tuple(
[Schema.String, Schema.optionalElement(Schema.Number)], // elements
Schema.Boolean // rest
)
ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Tuple(
[Schema.String, Schema.optionalElement(Schema.Number)], // elements
Schema.Boolean // rest
)

Optionally, you can include other elements after the rest:

ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Tuple(
[Schema.String, Schema.optionalElement(Schema.Number)], // elements
Schema.Boolean, // rest
Schema.String // additional element
)
ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Tuple(
[Schema.String, Schema.optionalElement(Schema.Number)], // elements
Schema.Boolean, // rest
Schema.String // additional element
)

Exposed Values

You can access the elements and rest elements of a tuple schema:

ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Tuple(
[Schema.String, Schema.optionalElement(Schema.Number)], // elements
Schema.Boolean, // rest
Schema.String // additional element
)
 
// Accesses the elements of the tuple
const tupleElements = schema.elements
 
// Accesses the rest elements of the tuple
const restElements = schema.rest
ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Tuple(
[Schema.String, Schema.optionalElement(Schema.Number)], // elements
Schema.Boolean, // rest
Schema.String // additional element
)
 
// Accesses the elements of the tuple
const tupleElements = schema.elements
 
// Accesses the rest elements of the tuple
const restElements = schema.rest

Annotations

Annotations are used to add metadata to tuple elements, which can describe the purpose or requirements of each element more clearly. This can be particularly useful when generating documentation or JSON schemas from your schemas.

ts
import { JSONSchema, Schema } from "@effect/schema"
 
// Defining a tuple with annotations for each coordinate in a point
const Point = Schema.Tuple(
Schema.element(Schema.Number).annotations({
title: "X",
description: "X coordinate"
}),
Schema.optionalElement(Schema.Number).annotations({
title: "Y",
description: "optional Y coordinate"
})
)
 
// Generating a JSON Schema from the tuple
console.log(JSONSchema.make(Point))
/*
Output:
{
'$schema': 'http://json-schema.org/draft-07/schema#',
type: 'array',
minItems: 1,
items: [
{ type: 'number', description: 'X coordinate', title: 'X' },
{
type: 'number',
description: 'optional Y coordinate',
title: 'Y'
}
],
additionalItems: false
}
*/
ts
import { JSONSchema, Schema } from "@effect/schema"
 
// Defining a tuple with annotations for each coordinate in a point
const Point = Schema.Tuple(
Schema.element(Schema.Number).annotations({
title: "X",
description: "X coordinate"
}),
Schema.optionalElement(Schema.Number).annotations({
title: "Y",
description: "optional Y coordinate"
})
)
 
// Generating a JSON Schema from the tuple
console.log(JSONSchema.make(Point))
/*
Output:
{
'$schema': 'http://json-schema.org/draft-07/schema#',
type: 'array',
minItems: 1,
items: [
{ type: 'number', description: 'X coordinate', title: 'X' },
{
type: 'number',
description: 'optional Y coordinate',
title: 'Y'
}
],
additionalItems: false
}
*/

Arrays

ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Array(Schema.Number)
ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Array(Schema.Number)

Exposed Values

You can access the value of an array schema:

ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Array(Schema.String)
 
// Accesses the value
const value = schema.value // typeof Schema.String
ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Array(Schema.String)
 
// Accesses the value
const value = schema.value // typeof Schema.String

Mutable Arrays

By default, when you use Schema.Array, it generates a type marked as readonly. The mutable combinator is a useful function for creating a new schema with a mutable type in a shallow manner:

ts
import { Schema } from "@effect/schema"
 
const schema = Schema.mutable(Schema.Array(Schema.Number))
ts
import { Schema } from "@effect/schema"
 
const schema = Schema.mutable(Schema.Array(Schema.Number))

Non empty arrays

ts
import { Schema } from "@effect/schema"
 
const schema = Schema.NonEmptyArray(Schema.Number)
ts
import { Schema } from "@effect/schema"
 
const schema = Schema.NonEmptyArray(Schema.Number)

Exposed Values

You can access the value of a non-empty array schema:

ts
import { Schema } from "@effect/schema"
 
const schema = Schema.NonEmptyArray(Schema.String)
 
// Accesses the value
const value = schema.value
ts
import { Schema } from "@effect/schema"
 
const schema = Schema.NonEmptyArray(Schema.String)
 
// Accesses the value
const value = schema.value

Records

String Keyed Records

ts
import { Schema } from "@effect/schema"
 
const opaque = Schema.Record({ key: Schema.String, value: Schema.Number })
 
// Schema<{ readonly [x: string]: number; }>
const schema = Schema.asSchema(opaque)
ts
import { Schema } from "@effect/schema"
 
const opaque = Schema.Record({ key: Schema.String, value: Schema.Number })
 
// Schema<{ readonly [x: string]: number; }>
const schema = Schema.asSchema(opaque)

Union of Literals as Keys

ts
import { Schema } from "@effect/schema"
 
const opaque = Schema.Record({
key: Schema.Union(Schema.Literal("a"), Schema.Literal("b")),
value: Schema.Number
})
 
// Schema<{ readonly a: number; readonly b: number; }>
const schema = Schema.asSchema(opaque)
ts
import { Schema } from "@effect/schema"
 
const opaque = Schema.Record({
key: Schema.Union(Schema.Literal("a"), Schema.Literal("b")),
value: Schema.Number
})
 
// Schema<{ readonly a: number; readonly b: number; }>
const schema = Schema.asSchema(opaque)

Applying Key Refinements

ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Record({
key: Schema.String.pipe(Schema.minLength(2)),
value: Schema.Number
})
ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Record({
key: Schema.String.pipe(Schema.minLength(2)),
value: Schema.Number
})

Symbol Keyed Records

ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Record({
key: Schema.SymbolFromSelf,
value: Schema.Number
})
ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Record({
key: Schema.SymbolFromSelf,
value: Schema.Number
})

Employing Template Literal Keys

ts
import { Schema } from "@effect/schema"
 
const opaque = Schema.Record({
key: Schema.TemplateLiteral(Schema.Literal("a"), Schema.String),
value: Schema.Number
})
 
// Schema<{ readonly [x: `a${string}`]: number; }>
const schema = Schema.asSchema(opaque)
ts
import { Schema } from "@effect/schema"
 
const opaque = Schema.Record({
key: Schema.TemplateLiteral(Schema.Literal("a"), Schema.String),
value: Schema.Number
})
 
// Schema<{ readonly [x: `a${string}`]: number; }>
const schema = Schema.asSchema(opaque)

Creating Mutable Records

By default, when you use Schema.Record, it generates a type marked as readonly. The mutable combinator is a useful function for creating a new schema with a mutable type in a shallow manner:

ts
import { Schema } from "@effect/schema"
 
const schema = Schema.mutable(
Schema.Record({ key: Schema.String, value: Schema.Number })
)
ts
import { Schema } from "@effect/schema"
 
const schema = Schema.mutable(
Schema.Record({ key: Schema.String, value: Schema.Number })
)

Exposed Values

You can access the key and the value of a record schema:

ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Record({ key: Schema.String, value: Schema.Number })
 
// Accesses the key
const key = schema.key
 
// Accesses the value
const value = schema.value
ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Record({ key: Schema.String, value: Schema.Number })
 
// Accesses the key
const key = schema.key
 
// Accesses the value
const value = schema.value

Structs

Structs are used to define schemas for objects with specific properties. Here's how you can create and use a struct schema:

ts
import { Schema } from "@effect/schema"
 
// Define a struct schema for an object with properties "name" (string) and "age" (number)
const MyStruct = Schema.Struct({
name: Schema.String,
age: Schema.Number
})
ts
import { Schema } from "@effect/schema"
 
// Define a struct schema for an object with properties "name" (string) and "age" (number)
const MyStruct = Schema.Struct({
name: Schema.String,
age: Schema.Number
})

The MyStruct constant will have the type

ts
const MyStruct: Schema.Struct<{
name: typeof Schema.String
age: typeof Schema.Number
}>
ts
const MyStruct: Schema.Struct<{
name: typeof Schema.String
age: typeof Schema.Number
}>

representing the structure of the object.

To view the detailed type of MyStruct, you can use the Schema.asSchema function:

ts
/*
const schema: Schema.Schema<{
readonly name: string;
readonly age: number;
}, {
readonly name: string;
readonly age: number;
}, never>
*/
const schema = Schema.asSchema(MyStruct)
ts
/*
const schema: Schema.Schema<{
readonly name: string;
readonly age: number;
}, {
readonly name: string;
readonly age: number;
}, never>
*/
const schema = Schema.asSchema(MyStruct)

Note that Schema.Struct({}) models the TypeScript type {}, which is similar to unknown. This means that the schema will allow any type of data to pass through without validation.

Index Signatures

The Struct constructor optionally accepts a list of key/value pairs representing index signatures:

ts
(props, ...indexSignatures) => Struct<...>
ts
(props, ...indexSignatures) => Struct<...>

Example

ts
import { Schema } from "@effect/schema"
 
/*
Schema.TypeLiteral<{
a: typeof Schema.Number;
}, readonly [{
readonly key: typeof Schema.String;
readonly value: typeof Schema.Number;
}]>
*/
const opaque = Schema.Struct(
{
a: Schema.Number
},
{ key: Schema.String, value: Schema.Number }
)
 
/*
Schema.Schema<{
readonly [x: string]: number;
readonly a: number;
}, {
readonly [x: string]: number;
readonly a: number;
}, never>
*/
const nonOpaque = Schema.asSchema(opaque)
ts
import { Schema } from "@effect/schema"
 
/*
Schema.TypeLiteral<{
a: typeof Schema.Number;
}, readonly [{
readonly key: typeof Schema.String;
readonly value: typeof Schema.Number;
}]>
*/
const opaque = Schema.Struct(
{
a: Schema.Number
},
{ key: Schema.String, value: Schema.Number }
)
 
/*
Schema.Schema<{
readonly [x: string]: number;
readonly a: number;
}, {
readonly [x: string]: number;
readonly a: number;
}, never>
*/
const nonOpaque = Schema.asSchema(opaque)

Since the Schema.Record constructor returns a schema that exposes both the key and the value, instead of passing a bare object { key, value }, you can use the Record constructor:

ts
import { Schema } from "@effect/schema"
 
/*
Schema.TypeLiteral<{
a: typeof Schema.Number;
}, readonly [Schema.Record$<typeof Schema.String, typeof Schema.Number>]>
*/
const opaque = Schema.Struct(
{ a: Schema.Number },
Schema.Record({ key: Schema.String, value: Schema.Number })
)
 
/*
Schema.Schema<{
readonly [x: string]: number;
readonly a: number;
}, {
readonly [x: string]: number;
readonly a: number;
}, never>
*/
const nonOpaque = Schema.asSchema(opaque)
ts
import { Schema } from "@effect/schema"
 
/*
Schema.TypeLiteral<{
a: typeof Schema.Number;
}, readonly [Schema.Record$<typeof Schema.String, typeof Schema.Number>]>
*/
const opaque = Schema.Struct(
{ a: Schema.Number },
Schema.Record({ key: Schema.String, value: Schema.Number })
)
 
/*
Schema.Schema<{
readonly [x: string]: number;
readonly a: number;
}, {
readonly [x: string]: number;
readonly a: number;
}, never>
*/
const nonOpaque = Schema.asSchema(opaque)

Exposed Values

You can access the fields and the records of a struct schema:

ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Struct(
{ a: Schema.Number },
Schema.Record({ key: Schema.String, value: Schema.Number })
)
 
// Accesses the fields
const fields = schema.fields
 
// Accesses the records
const records = schema.records
ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Struct(
{ a: Schema.Number },
Schema.Record({ key: Schema.String, value: Schema.Number })
)
 
// Accesses the fields
const fields = schema.fields
 
// Accesses the records
const records = schema.records

Mutable Properties

By default, when you use Schema.Struct, it generates a type with properties that are marked as readonly. The Schema.mutable combinator is a useful function for creating a new schema with properties made mutable in a shallow manner:

ts
import { Schema } from "@effect/schema"
 
const opaque = Schema.mutable(
Schema.Struct({ a: Schema.String, b: Schema.Number })
)
 
/*
Schema.Schema<{
a: string;
b: number;
}, {
a: string;
b: number;
}, never>
*/
const nonOpaque = Schema.asSchema(opaque)
ts
import { Schema } from "@effect/schema"
 
const opaque = Schema.mutable(
Schema.Struct({ a: Schema.String, b: Schema.Number })
)
 
/*
Schema.Schema<{
a: string;
b: number;
}, {
a: string;
b: number;
}, never>
*/
const nonOpaque = Schema.asSchema(opaque)

Property Signatures

Basic Usage of Property Signatures

A PropertySignature generally represents a transformation from a "From" field:

ts
{
fromKey: fromType
}
ts
{
fromKey: fromType
}

to a "To" field:

ts
{
toKey: toType
}
ts
{
toKey: toType
}

Let's start with the simple definition of a property signature that can be used to add annotations:

ts
import { Schema } from "@effect/schema"
 
/*
Schema.Struct<{
name: typeof Schema.String;
age: Schema.propertySignature<typeof Schema.NumberFromString>;
}>
*/
const Person = Schema.Struct({
name: Schema.String,
age: Schema.propertySignature(Schema.NumberFromString).annotations({
title: "Age"
})
})
ts
import { Schema } from "@effect/schema"
 
/*
Schema.Struct<{
name: typeof Schema.String;
age: Schema.propertySignature<typeof Schema.NumberFromString>;
}>
*/
const Person = Schema.Struct({
name: Schema.String,
age: Schema.propertySignature(Schema.NumberFromString).annotations({
title: "Age"
})
})

Let's delve into the details of all the information contained in the type of a PropertySignature:

ts
age: PropertySignature<
ToToken,
ToType,
FromKey,
FromToken,
FromType,
HasDefault,
Context
>
ts
age: PropertySignature<
ToToken,
ToType,
FromKey,
FromToken,
FromType,
HasDefault,
Context
>
Param NameDescription
ageKey of the "To" field
ToTokenIndicates field requirement: "?:" for optional, ":" for required
ToTypeType of the "To" field
FromKey(Optional, default = never) Indicates the source field key, typically the same as "To" field key unless specified
FormTokenIndicates source field requirement: "?:" for optional, ":" for required
FromTypeType of the "From" field
HasDefaultIndicates if there is a constructor default value (Boolean)

In our case, the type

ts
PropertySignature<":", number, never, ":", string, false, never>
ts
PropertySignature<":", number, never, ":", string, false, never>

indicates that there is the following transformation:

Param NameDescription
ageKey of the "To" field
ToToken":" indicates that the age field is required
ToTypeType of the age field is number
FromKeynever indicates that the decoding occurs from the same field named age
FormToken":" indicates that the decoding occurs from a required age field
FromTypeType of the "From" field is string
HasDefaultfalse: indicates there is no default value

Now, suppose the field from which decoding occurs is named "AGE", but for our model, we want to keep the name in lowercase "age". To achieve this result, we need to map the field key from "AGE" to "age", and to do that, we can use the fromKey combinator:

ts
import { Schema } from "@effect/schema"
 
/*
Schema.Struct<{
name: typeof Schema.String;
age: Schema.PropertySignature<":", number, "AGE", ":", string, false, never>;
}>
*/
const Person = Schema.Struct({
name: Schema.String,
age: Schema.propertySignature(Schema.NumberFromString).pipe(
Schema.fromKey("AGE")
)
})
ts
import { Schema } from "@effect/schema"
 
/*
Schema.Struct<{
name: typeof Schema.String;
age: Schema.PropertySignature<":", number, "AGE", ":", string, false, never>;
}>
*/
const Person = Schema.Struct({
name: Schema.String,
age: Schema.propertySignature(Schema.NumberFromString).pipe(
Schema.fromKey("AGE")
)
})

This modification is represented in the type of the created PropertySignature:

ts
// fromKey ----------------------v
PropertySignature<":", number, "AGE", ":", string, false, never>
ts
// fromKey ----------------------v
PropertySignature<":", number, "AGE", ":", string, false, never>

Now, let's see an example of decoding:

ts
console.log(Schema.decodeUnknownSync(Person)({ name: "name", AGE: "18" }))
// Output: { name: 'name', age: 18 }
ts
console.log(Schema.decodeUnknownSync(Person)({ name: "name", AGE: "18" }))
// Output: { name: 'name', age: 18 }

Optional Fields

Basic Optional Property

Schema.optional(schema: Schema<A, I, R>) defines a basic optional property that handles different inputs and outputs during decoding and encoding:

  • Decoding:
    • <missing value> remains <missing value>
    • undefined remains undefined
    • Input i: I transforms to a: A
  • Encoding:
    • <missing value> remains <missing value>
    • undefined remains undefined
    • Input a: A transforms back to i: I

Optional with Nullability

Schema.optionalWith(schema: Schema<A, I, R>, { nullable: true }) allows handling of null values as equivalent to missing values:

  • Decoding:
    • <missing value> remains <missing value>
    • undefined remains undefined
    • null transforms to <missing value>
    • Input i: I transforms to a: A
  • Encoding:
    • <missing value> remains <missing value>
    • undefined remains undefined
    • Input a: A transforms back to i: I

Optional with Exactness

Schema.optionalWith(schema: Schema<A, I, R>, { exact: true }) ensures that only the exact types specified are handled, excluding undefined:

  • Decoding:
    • <missing value> remains <missing value>
    • Input i: I transforms to a: A
  • Encoding:
    • <missing value> remains <missing value>
    • Input a: A transforms back to i: I

Combining Nullability and Exactness

Schema.optionalWith(schema: Schema<A, I, R>, { exact: true, nullable: true }) combines handling for exact types and null values:

  • Decoding:
    • <missing value> remains <missing value>
    • null transforms to <missing value>
    • Input i: I transforms to a: A
  • Encoding:
    • <missing value> remains <missing value>
    • Input a: A transforms back to i: I

Representing Optional Fields with never Type

When defining types in TypeScript that include optional fields with the type never, such as:

ts
type A = {
readonly a?: never
}
ts
type A = {
readonly a?: never
}

the approach varies based on the exactOptionalPropertyTypes configuration in your tsconfig.json

TypeScript Configuration: exactOptionalPropertyTypes = false

When this setting is disabled, optional fields are best handled by explicitly defining them as undefined. This reflects that the field may not be present, and if it is, it holds no value.

ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Struct({
a: Schema.optional(Schema.Undefined)
})
ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Struct({
a: Schema.optional(Schema.Undefined)
})

TypeScript Configuration: exactOptionalPropertyTypes = true

With exactOptionalPropertyTypes enabled, the definition changes slightly to enforce the field's absence more strictly. This configuration is more precise and aligns with newer TypeScript strictness features.

ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Struct({
a: Schema.optionalWith(Schema.Never, { exact: true })
})
ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Struct({
a: Schema.optionalWith(Schema.Never, { exact: true })
})

Default Values

The default option in schemas allows you to set default values that are applied during both decoding and object construction phases. This feature ensures that even if certain properties are not provided by the user, the system will automatically use the specified default values.

Example

Let's see how default values work in both the decoding and constructing phases, illustrating how the default value is applied when certain properties are not provided.

ts
import { Schema } from "@effect/schema"
 
const Product = Schema.Struct({
name: Schema.String,
price: Schema.NumberFromString,
quantity: Schema.optionalWith(Schema.NumberFromString, { default: () => 1 })
})
 
// Applying defaults in the decoding phase
 
console.log(
Schema.decodeUnknownSync(Product)({ name: "Laptop", price: "999" })
)
// Output: { name: 'Laptop', price: 999, quantity: 1 }
 
console.log(
Schema.decodeUnknownSync(Product)({
name: "Laptop",
price: "999",
quantity: "2"
})
)
// Output: { name: 'Laptop', price: 999, quantity: 2 }
 
// Applying defaults in the constructor
 
console.log(Product.make({ name: "Laptop", price: 999 }))
// Output: { name: 'Laptop', price: 999, quantity: 1 }
 
console.log(Product.make({ name: "Laptop", price: 999, quantity: 2 }))
// Output: { name: 'Laptop', price: 999, quantity: 2 }
ts
import { Schema } from "@effect/schema"
 
const Product = Schema.Struct({
name: Schema.String,
price: Schema.NumberFromString,
quantity: Schema.optionalWith(Schema.NumberFromString, { default: () => 1 })
})
 
// Applying defaults in the decoding phase
 
console.log(
Schema.decodeUnknownSync(Product)({ name: "Laptop", price: "999" })
)
// Output: { name: 'Laptop', price: 999, quantity: 1 }
 
console.log(
Schema.decodeUnknownSync(Product)({
name: "Laptop",
price: "999",
quantity: "2"
})
)
// Output: { name: 'Laptop', price: 999, quantity: 2 }
 
// Applying defaults in the constructor
 
console.log(Product.make({ name: "Laptop", price: 999 }))
// Output: { name: 'Laptop', price: 999, quantity: 1 }
 
console.log(Product.make({ name: "Laptop", price: 999, quantity: 2 }))
// Output: { name: 'Laptop', price: 999, quantity: 2 }

Schema.optionalWith can be configured with additional options to handle decoding and encoding precisely:

Basic Optional with Default

Schema.optionalWith(schema: Schema<A, I, R>, { default: () => A })

  • Decoding: Translates missing or undefined inputs to a default value.
  • Encoding: Input a: A transforms back to i: I

Optional with Exactness

Schema.optionalWith(schema: Schema<A, I, R>, { exact: true, default: () => A })

  • Decoding: Applies the default value only if the input is missing.
  • Encoding: Input a: A transforms back to i: I

Optional with Nullability

Schema.optionalWith(schema: Schema<A, I, R>, { nullable: true, default: () => A })

  • Decoding: Treats null, undefined, or missing inputs as defaults.
  • Encoding: Input a: A transforms back to i: I

Combining Exactness and Nullability

Schema.optionalWith(schema: Schema<A, I, R>, { exact: true, nullable: true, default: () => A })

  • Decoding: Defaults are applied when values are null or missing.
  • Encoding: Input a: A transforms back to i: I

Optional Fields as Options

Basic Optional with Option Type

optionalWith(schema: Schema<A, I, R>, { as: "Option" })

  • Decoding:
    • Missing values or undefined are converted to Option.none().
    • Provided values (i: I) are converted to Option.some(a: A).
  • Encoding:
    • Option.none() results in the value being omitted.
    • Option.some(a: A) is converted back to the original input (i: I).

Optional with Exactness

optionalWith(schema: Schema<A, I, R>, { exact: true, as: "Option" })

  • Decoding:
    • Only truly missing values are converted to Option.none().
    • Provided values (i: I) are converted to Option.some(a).
  • Encoding:
    • Option.none() results in the value being omitted.
    • Option.some(a: A) is converted back to the original input (i: I).

Optional with Nullability

optionalWith(schema: Schema<A, I, R>, { nullable: true, as: "Option" })

  • Decoding:
    • Treats missing, undefined, and null values all as Option.none().
    • Provided values (i: I) are converted to Option.some(a: A).
  • Encoding:
    • Option.none() results in the value being omitted.
    • Option.some(a: A) is converted back to the original input (i: I).

Combining Exactness and Nullability

optionalWith(schema: Schema<A, I, R>, { exact: true, nullable: true, as: "Option" })

  • Decoding:
    • Missing or null values lead to Option.none().
    • Provided values (i: I) are converted to Option.some(a: A).
  • Encoding:
    • Option.none() results in the value being omitted.
    • Option.some(a: A) is converted back to the original input (i: I).

Optional Fields Primitives

The Schema.optional and Schema.optionalWith functions are built on two foundational operations: Schema.optionalToOptional and Schema.optionalToRequired. These functions provide nuanced control over how optional fields are handled in your schemas, allowing for precise property signatures.

optionalToOptional

The Schema.optionalToOptional API is used to manage the transformation from an optional field to another optional field. With this, we can control both the output type and the presence or absence of the field.

For example a common use case is to equate a specific value in the source field with the absence of value in the destination field.

Here's the signature of the optionalToOptional API:

ts
export const optionalToOptional = <FA, FI, FR, TA, TI, TR>(
from: Schema<FA, FI, FR>,
to: Schema<TA, TI, TR>,
options: {
readonly decode: (o: Option.Option<FA>) => Option.Option<TI>,
readonly encode: (o: Option.Option<TI>) => Option.Option<FA>
}
): PropertySignature<"?:", TA, never, "?:", FI, false, FR | TR>
ts
export const optionalToOptional = <FA, FI, FR, TA, TI, TR>(
from: Schema<FA, FI, FR>,
to: Schema<TA, TI, TR>,
options: {
readonly decode: (o: Option.Option<FA>) => Option.Option<TI>,
readonly encode: (o: Option.Option<TI>) => Option.Option<FA>
}
): PropertySignature<"?:", TA, never, "?:", FI, false, FR | TR>

As you can see, we can transform the type by specifying a schema for to, which can be different from the schema of from. Additionally, we can control the presence or absence of the field using decode and encode, with the following meanings:

  • Option.none() as an argument means the value is missing in the input
  • Option.none() as a return value means the value will be missing in the output

Example

Suppose we have an optional field of type string, and we want to exclude empty strings from the output. In other words, if the input contains an empty string, we want the field to be absent in the output.

ts
import { Schema } from "@effect/schema"
import { identity, Option } from "effect"
 
const schema = Schema.Struct({
a: Schema.optionalToOptional(Schema.String, Schema.String, {
decode: (input) => {
if (Option.isNone(input)) {
// If the field is absent in the input, returning `Option.none()` will make it absent in the output too
return Option.none()
}
const value = input.value
if (value === "") {
// If the field is present in the input but is an empty string, returning `Option.none()` will make it absent in the output
return Option.none()
}
// If the field is present in the input and is not an empty string, returning `Option.some` will make it present in the output
return Option.some(value)
},
// Here in the encoding part, we can decide to handle things in the same way as in the decoding phase
// or handle them differently. For example, we can leave everything unchanged and use the identity function
encode: identity
})
})
 
const decode = Schema.decodeUnknownSync(schema)
 
console.log(decode({})) // Output: {}
console.log(decode({ a: "" })) // Output: {}
console.log(decode({ a: "a non-empty string" })) // Output: { a: 'a non-empty string' }
 
const encode = Schema.encodeSync(schema)
 
console.log(encode({})) // Output: {}
console.log(encode({ a: "" })) // Output: { a: '' }
console.log(encode({ a: "foo" })) // Output: { a: 'foo' }
ts
import { Schema } from "@effect/schema"
import { identity, Option } from "effect"
 
const schema = Schema.Struct({
a: Schema.optionalToOptional(Schema.String, Schema.String, {
decode: (input) => {
if (Option.isNone(input)) {
// If the field is absent in the input, returning `Option.none()` will make it absent in the output too
return Option.none()
}
const value = input.value
if (value === "") {
// If the field is present in the input but is an empty string, returning `Option.none()` will make it absent in the output
return Option.none()
}
// If the field is present in the input and is not an empty string, returning `Option.some` will make it present in the output
return Option.some(value)
},
// Here in the encoding part, we can decide to handle things in the same way as in the decoding phase
// or handle them differently. For example, we can leave everything unchanged and use the identity function
encode: identity
})
})
 
const decode = Schema.decodeUnknownSync(schema)
 
console.log(decode({})) // Output: {}
console.log(decode({ a: "" })) // Output: {}
console.log(decode({ a: "a non-empty string" })) // Output: { a: 'a non-empty string' }
 
const encode = Schema.encodeSync(schema)
 
console.log(encode({})) // Output: {}
console.log(encode({ a: "" })) // Output: { a: '' }
console.log(encode({ a: "foo" })) // Output: { a: 'foo' }

optionalToRequired

The Schema.optionalToRequired API allows us to transform an optional field into a required one, applying custom logic if the field is absent in the input.

ts
export const optionalToRequired = <FA, FI, FR, TA, TI, TR>(
from: Schema<FA, FI, FR>,
to: Schema<TA, TI, TR>,
options: {
readonly decode: (o: Option.Option<FA>) => TI,
readonly encode: (ti: TI) => Option.Option<FA>
}
): PropertySignature<":", TA, never, "?:", FI, false, FR | TR>
ts
export const optionalToRequired = <FA, FI, FR, TA, TI, TR>(
from: Schema<FA, FI, FR>,
to: Schema<TA, TI, TR>,
options: {
readonly decode: (o: Option.Option<FA>) => TI,
readonly encode: (ti: TI) => Option.Option<FA>
}
): PropertySignature<":", TA, never, "?:", FI, false, FR | TR>

We can control the presence or absence of the field using decode and encode, with the following meanings:

  • Option.none() as an argument means the value is missing in the input
  • Option.none() as a return value means the value will be missing in the output

Example

For instance, a common use case is to assign a default value to the field in the output if it's missing in the input.

ts
import { Schema } from "@effect/schema"
import { Option } from "effect"
 
const schema = Schema.Struct({
a: Schema.optionalToRequired(Schema.String, Schema.String, {
decode: (input) => {
if (Option.isNone(input)) {
// If the field is absent in the input, we can return the default value for the field in the output
return "default value"
}
// If the field is present in the input, return its value as it is in the output
return input.value
},
// During encoding, we can choose to handle things differently, or simply return the same value present in the input for the output
encode: (a) => Option.some(a)
})
})
 
const decode = Schema.decodeUnknownSync(schema)
 
console.log(decode({})) // Output: { a: 'default value' }
console.log(decode({ a: "foo" })) // Output: { a: 'foo' }
 
const encode = Schema.encodeSync(schema)
 
console.log(encode({ a: "foo" })) // Output: { a: 'foo' }
ts
import { Schema } from "@effect/schema"
import { Option } from "effect"
 
const schema = Schema.Struct({
a: Schema.optionalToRequired(Schema.String, Schema.String, {
decode: (input) => {
if (Option.isNone(input)) {
// If the field is absent in the input, we can return the default value for the field in the output
return "default value"
}
// If the field is present in the input, return its value as it is in the output
return input.value
},
// During encoding, we can choose to handle things differently, or simply return the same value present in the input for the output
encode: (a) => Option.some(a)
})
})
 
const decode = Schema.decodeUnknownSync(schema)
 
console.log(decode({})) // Output: { a: 'default value' }
console.log(decode({ a: "foo" })) // Output: { a: 'foo' }
 
const encode = Schema.encodeSync(schema)
 
console.log(encode({ a: "foo" })) // Output: { a: 'foo' }

requiredToOptional

This API allows developers to specify how a field that is normally required can be treated as optional based on custom logic.

ts
export const requiredToOptional = <FA, FI, FR, TA, TI, TR>(
from: Schema<FA, FI, FR>,
to: Schema<TA, TI, TR>,
options: {
readonly decode: (fa: FA) => Option.Option<TI>
readonly encode: (o: Option.Option<TI>) => FA
}
): PropertySignature<"?:", TA, never, ":", FI, false, FR | TR>
ts
export const requiredToOptional = <FA, FI, FR, TA, TI, TR>(
from: Schema<FA, FI, FR>,
to: Schema<TA, TI, TR>,
options: {
readonly decode: (fa: FA) => Option.Option<TI>
readonly encode: (o: Option.Option<TI>) => FA
}
): PropertySignature<"?:", TA, never, ":", FI, false, FR | TR>

We can control the presence or absence of the field using decode and encode, with the following meanings:

  • Option.none() as an argument means the value is missing in the input
  • Option.none() as a return value means the value will be missing in the output

Example

Let's look at a practical example where a field name that is typically required can be considered optional if it's an empty string during decoding, and ensure there is always a value during encoding by providing a default.

ts
import { Schema } from "@effect/schema"
import { Option } from "effect"
 
const schema = Schema.Struct({
name: Schema.requiredToOptional(Schema.String, Schema.String, {
decode: Option.liftPredicate((s) => s !== ""), // empty string is considered as absent
encode: Option.getOrElse(() => "")
})
})
 
const decode = Schema.decodeUnknownSync(schema)
 
console.log(decode({ name: "John" })) // Output: { name: 'John' }
console.log(decode({ name: "" })) // Output: {}
 
const encode = Schema.encodeSync(schema)
 
console.log(encode({ name: "John" })) // { name: 'John' }
console.log(encode({})) // Output: { name: '' }
ts
import { Schema } from "@effect/schema"
import { Option } from "effect"
 
const schema = Schema.Struct({
name: Schema.requiredToOptional(Schema.String, Schema.String, {
decode: Option.liftPredicate((s) => s !== ""), // empty string is considered as absent
encode: Option.getOrElse(() => "")
})
})
 
const decode = Schema.decodeUnknownSync(schema)
 
console.log(decode({ name: "John" })) // Output: { name: 'John' }
console.log(decode({ name: "" })) // Output: {}
 
const encode = Schema.encodeSync(schema)
 
console.log(encode({ name: "John" })) // { name: 'John' }
console.log(encode({})) // Output: { name: '' }

Renaming Properties

Renaming a Property During Definition

To rename a property directly during schema creation, you can utilize the Schema.fromKey function. This function is particularly useful when you want to map properties from the input object to different names in the resulting schema object.

Example: Renaming a Required Property

ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Struct({
a: Schema.propertySignature(Schema.String).pipe(Schema.fromKey("c")),
b: Schema.Number
})
 
console.log(Schema.decodeUnknownSync(schema)({ c: "c", b: 1 }))
// Output: { a: "c", b: 1 }
ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Struct({
a: Schema.propertySignature(Schema.String).pipe(Schema.fromKey("c")),
b: Schema.Number
})
 
console.log(Schema.decodeUnknownSync(schema)({ c: "c", b: 1 }))
// Output: { a: "c", b: 1 }

Example: Renaming an Optional Property

ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Struct({
a: Schema.optional(Schema.String).pipe(Schema.fromKey("c")),
b: Schema.Number
})
 
console.log(Schema.decodeUnknownSync(schema)({ c: "c", b: 1 }))
// Output: { b: 1, a: "c" }
 
console.log(Schema.decodeUnknownSync(schema)({ b: 1 }))
// Output: { b: 1 }
ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Struct({
a: Schema.optional(Schema.String).pipe(Schema.fromKey("c")),
b: Schema.Number
})
 
console.log(Schema.decodeUnknownSync(schema)({ c: "c", b: 1 }))
// Output: { b: 1, a: "c" }
 
console.log(Schema.decodeUnknownSync(schema)({ b: 1 }))
// Output: { b: 1 }

Note that Schema.optional returns a PropertySignature, which simplifies the process by eliminating the need for explicit Schema.propertySignature usage as required in the previous example.

Renaming Properties of an Existing Schema

For existing schemas, the rename API offers a way to systematically change property names across a schema, even within complex structures like unions.

Example: Renaming Properties in a Struct Schema

ts
import { Schema } from "@effect/schema"
 
// Original Schema
const originalSchema = Schema.Struct({
c: Schema.String,
b: Schema.Number
})
 
// Renaming the "c" property to "a"
const renamedSchema = Schema.rename(originalSchema, { c: "a" })
 
console.log(Schema.decodeUnknownSync(renamedSchema)({ c: "c", b: 1 }))
// Output: { a: "c", b: 1 }
ts
import { Schema } from "@effect/schema"
 
// Original Schema
const originalSchema = Schema.Struct({
c: Schema.String,
b: Schema.Number
})
 
// Renaming the "c" property to "a"
const renamedSchema = Schema.rename(originalSchema, { c: "a" })
 
console.log(Schema.decodeUnknownSync(renamedSchema)({ c: "c", b: 1 }))
// Output: { a: "c", b: 1 }

Example: Renaming Properties in Union Schemas

ts
import { Schema } from "@effect/schema"
 
const originalSchema = Schema.Union(
Schema.Struct({
c: Schema.String,
b: Schema.Number
}),
Schema.Struct({
c: Schema.String,
d: Schema.Boolean
})
)
 
// Renaming the "c" property to "a" for all members
const renamedSchema = Schema.rename(originalSchema, { c: "a" })
 
console.log(Schema.decodeUnknownSync(renamedSchema)({ c: "c", b: 1 }))
// Output: { a: "c", b: 1 }
 
console.log(Schema.decodeUnknownSync(renamedSchema)({ c: "c", d: false }))
// Output: { d: false, a: 'c' }
ts
import { Schema } from "@effect/schema"
 
const originalSchema = Schema.Union(
Schema.Struct({
c: Schema.String,
b: Schema.Number
}),
Schema.Struct({
c: Schema.String,
d: Schema.Boolean
})
)
 
// Renaming the "c" property to "a" for all members
const renamedSchema = Schema.rename(originalSchema, { c: "a" })
 
console.log(Schema.decodeUnknownSync(renamedSchema)({ c: "c", b: 1 }))
// Output: { a: "c", b: 1 }
 
console.log(Schema.decodeUnknownSync(renamedSchema)({ c: "c", d: false }))
// Output: { d: false, a: 'c' }

Tagged Structs

In TypeScript tags help to enhance type discrimination and pattern matching by providing a simple yet powerful way to define and recognize different data types.

What is a Tag?

A tag is a literal value added to data structures, commonly used in structs, to distinguish between various object types or variants within tagged unions. This literal acts as a discriminator, making it easier to handle and process different types of data correctly and efficiently.

Using the tag Constructor

The tag constructor is specifically designed to create a property signature that holds a specific literal value, serving as the discriminator for object types. Here's how you can define a schema with a tag:

ts
import { Schema } from "@effect/schema"
 
const User = Schema.Struct({
_tag: Schema.tag("User"),
name: Schema.String,
age: Schema.Number
})
 
console.log(User.make({ name: "John", age: 44 }))
/*
Output:
{ _tag: 'User', name: 'John', age: 44 }
*/
ts
import { Schema } from "@effect/schema"
 
const User = Schema.Struct({
_tag: Schema.tag("User"),
name: Schema.String,
age: Schema.Number
})
 
console.log(User.make({ name: "John", age: 44 }))
/*
Output:
{ _tag: 'User', name: 'John', age: 44 }
*/

In the example above, Schema.tag("User") attaches a _tag property to the User struct schema, effectively labeling objects of this struct type as "User". This label is automatically applied when using the make method to create new instances, simplifying object creation and ensuring consistent tagging.

Simplifying Tagged Structs with TaggedStruct

The TaggedStruct constructor streamlines the process of creating tagged structs by directly integrating the tag into the struct definition. This method provides a clearer and more declarative approach to building data structures with embedded discriminators.

ts
import { Schema } from "@effect/schema"
 
const User = Schema.TaggedStruct("User", {
name: Schema.String,
age: Schema.Number
})
 
// `_tag` is optional
const userInstance = User.make({ name: "John", age: 44 })
 
console.log(userInstance)
/*
Output:
{ _tag: 'User', name: 'John', age: 44 }
*/
ts
import { Schema } from "@effect/schema"
 
const User = Schema.TaggedStruct("User", {
name: Schema.String,
age: Schema.Number
})
 
// `_tag` is optional
const userInstance = User.make({ name: "John", age: 44 })
 
console.log(userInstance)
/*
Output:
{ _tag: 'User', name: 'John', age: 44 }
*/

Multiple Tags

While a primary tag is often sufficient, TypeScript allows you to define multiple tags for more complex data structuring needs. Here's an example demonstrating the use of multiple tags within a single struct:

ts
import { Schema } from "@effect/schema"
 
const Product = Schema.TaggedStruct("Product", {
category: Schema.tag("Electronics"),
name: Schema.String,
price: Schema.Number
})
 
// `_tag` and `category` are optional
const productInstance = Product.make({ name: "Smartphone", price: 999 })
 
console.log(productInstance)
/*
Output:
{
_tag: 'Product',
category: 'Electronics',
name: 'Smartphone',
price: 999
}
*/
ts
import { Schema } from "@effect/schema"
 
const Product = Schema.TaggedStruct("Product", {
category: Schema.tag("Electronics"),
name: Schema.String,
price: Schema.Number
})
 
// `_tag` and `category` are optional
const productInstance = Product.make({ name: "Smartphone", price: 999 })
 
console.log(productInstance)
/*
Output:
{
_tag: 'Product',
category: 'Electronics',
name: 'Smartphone',
price: 999
}
*/

This example showcases a product schema that not only categorizes each product under a general tag ("Product") but also specifies a category tag ("Electronics"), enhancing the clarity and specificity of the data model.

instanceOf

When you need to define a schema for your custom data type defined through a class, the most convenient and fast way is to use the Schema.instanceOf constructor.

Example

ts
import { Schema } from "@effect/schema"
 
class MyData {
constructor(readonly name: string) {}
}
 
// Schema.instanceOf<MyData>
const MyDataSchema = Schema.instanceOf(MyData)
 
console.log(Schema.decodeUnknownSync(MyDataSchema)(new MyData("name")))
// Output: MyData { name: 'name' }
 
console.log(Schema.decodeUnknownSync(MyDataSchema)({ name: "name" }))
/*
throws
ParseError: Expected MyData, actual {"name":"name"}
*/
ts
import { Schema } from "@effect/schema"
 
class MyData {
constructor(readonly name: string) {}
}
 
// Schema.instanceOf<MyData>
const MyDataSchema = Schema.instanceOf(MyData)
 
console.log(Schema.decodeUnknownSync(MyDataSchema)(new MyData("name")))
// Output: MyData { name: 'name' }
 
console.log(Schema.decodeUnknownSync(MyDataSchema)({ name: "name" }))
/*
throws
ParseError: Expected MyData, actual {"name":"name"}
*/

The Schema.instanceOf constructor is just a lightweight wrapper of the Schema.declare API, which is the primitive in @effect/schema for declaring new custom data types.

However, note that Schema.instanceOf can only be used for classes that expose a public constructor. If you try to use it with classes that, for some reason, have marked the constructor as private, you'll receive a TypeScript error:

ts
import { Schema } from "@effect/schema"
 
class MyData {
static make = (name: string) => new MyData(name)
private constructor(readonly name: string) {}
}
 
const MyDataSchema = Schema.instanceOf(MyData)
Argument of type 'typeof MyData' is not assignable to parameter of type 'abstract new (...args: any) => any'. Cannot assign a 'private' constructor type to a 'public' constructor type.2345Argument of type 'typeof MyData' is not assignable to parameter of type 'abstract new (...args: any) => any'. Cannot assign a 'private' constructor type to a 'public' constructor type.
ts
import { Schema } from "@effect/schema"
 
class MyData {
static make = (name: string) => new MyData(name)
private constructor(readonly name: string) {}
}
 
const MyDataSchema = Schema.instanceOf(MyData)
Argument of type 'typeof MyData' is not assignable to parameter of type 'abstract new (...args: any) => any'. Cannot assign a 'private' constructor type to a 'public' constructor type.2345Argument of type 'typeof MyData' is not assignable to parameter of type 'abstract new (...args: any) => any'. Cannot assign a 'private' constructor type to a 'public' constructor type.

In such cases, you cannot use Schema.instanceOf, and you must rely on Schema.declare like this:

ts
import { Schema } from "@effect/schema"
 
class MyData {
static make = (name: string) => new MyData(name)
private constructor(readonly name: string) {}
}
 
const MyDataSchema = Schema.declare(
(input: unknown): input is MyData => input instanceof MyData
)
 
console.log(Schema.decodeUnknownSync(MyDataSchema)(MyData.make("name")))
// Output: MyData { name: 'name' }
 
console.log(Schema.decodeUnknownSync(MyDataSchema)({ name: "name" }))
/*
throws
ParseError: Expected <declaration schema>, actual {"name":"name"}
*/
ts
import { Schema } from "@effect/schema"
 
class MyData {
static make = (name: string) => new MyData(name)
private constructor(readonly name: string) {}
}
 
const MyDataSchema = Schema.declare(
(input: unknown): input is MyData => input instanceof MyData
)
 
console.log(Schema.decodeUnknownSync(MyDataSchema)(MyData.make("name")))
// Output: MyData { name: 'name' }
 
console.log(Schema.decodeUnknownSync(MyDataSchema)({ name: "name" }))
/*
throws
ParseError: Expected <declaration schema>, actual {"name":"name"}
*/

To improve the error message in case of failed decoding, remember to add annotations:

ts
const MyDataSchema = Schema.declare(
(input: unknown): input is MyData => input instanceof MyData,
{ identifier: "MyData" } // annotations
)
 
console.log(Schema.decodeUnknownSync(MyDataSchema)({ name: "name" }))
/*
throws
ParseError: Expected MyData, actual {"name":"name"}
*/
ts
const MyDataSchema = Schema.declare(
(input: unknown): input is MyData => input instanceof MyData,
{ identifier: "MyData" } // annotations
)
 
console.log(Schema.decodeUnknownSync(MyDataSchema)({ name: "name" }))
/*
throws
ParseError: Expected MyData, actual {"name":"name"}
*/

pick

The pick static function available in each struct schema can be used to create a new Struct by selecting particular properties from an existing Struct.

ts
import { Schema } from "@effect/schema"
 
const MyStruct = Schema.Struct({
a: Schema.String,
b: Schema.Number,
c: Schema.Boolean
})
 
// Schema.Struct<{ a: typeof Schema.String; c: typeof Schema.Boolean; }>
const PickedSchema = MyStruct.pick("a", "c")
ts
import { Schema } from "@effect/schema"
 
const MyStruct = Schema.Struct({
a: Schema.String,
b: Schema.Number,
c: Schema.Boolean
})
 
// Schema.Struct<{ a: typeof Schema.String; c: typeof Schema.Boolean; }>
const PickedSchema = MyStruct.pick("a", "c")

The Schema.pick function can be applied more broadly beyond just Struct types, such as with unions of schemas. However it returns a generic SchemaClass.

Example: Picking from a Union

ts
import { Schema } from "@effect/schema"
 
const MyUnion = Schema.Union(
Schema.Struct({ a: Schema.String, b: Schema.String, c: Schema.String }),
Schema.Struct({ a: Schema.Number, b: Schema.Number, d: Schema.Number })
)
 
// Schema.Schema<{ readonly a: string | number; readonly b: string | number }>
const PickedSchema = MyUnion.pipe(Schema.pick("a", "b"))
ts
import { Schema } from "@effect/schema"
 
const MyUnion = Schema.Union(
Schema.Struct({ a: Schema.String, b: Schema.String, c: Schema.String }),
Schema.Struct({ a: Schema.Number, b: Schema.Number, d: Schema.Number })
)
 
// Schema.Schema<{ readonly a: string | number; readonly b: string | number }>
const PickedSchema = MyUnion.pipe(Schema.pick("a", "b"))

omit

The omit static function available in each struct schema can be used to create a new Struct by excluding particular properties from an existing Struct.

ts
import { Schema } from "@effect/schema"
 
const MyStruct = Schema.Struct({
a: Schema.String,
b: Schema.Number,
c: Schema.Boolean
})
 
// Schema.Struct<{ a: typeof Schema.String; c: typeof Schema.Boolean; }>
const PickedSchema = MyStruct.omit("b")
ts
import { Schema } from "@effect/schema"
 
const MyStruct = Schema.Struct({
a: Schema.String,
b: Schema.Number,
c: Schema.Boolean
})
 
// Schema.Struct<{ a: typeof Schema.String; c: typeof Schema.Boolean; }>
const PickedSchema = MyStruct.omit("b")

The Schema.omit function can be applied more broadly beyond just Struct types, such as with unions of schemas. However it returns a generic Schema.

ts
import { Schema } from "@effect/schema"
 
const MyUnion = Schema.Union(
Schema.Struct({ a: Schema.String, b: Schema.String, c: Schema.String }),
Schema.Struct({ a: Schema.Number, b: Schema.Number, d: Schema.Number })
)
 
// Schema<{ readonly a: string | number }>
const PickedSchema = MyUnion.pipe(Schema.omit("b"))
ts
import { Schema } from "@effect/schema"
 
const MyUnion = Schema.Union(
Schema.Struct({ a: Schema.String, b: Schema.String, c: Schema.String }),
Schema.Struct({ a: Schema.Number, b: Schema.Number, d: Schema.Number })
)
 
// Schema<{ readonly a: string | number }>
const PickedSchema = MyUnion.pipe(Schema.omit("b"))

partial

The Schema.partial operation makes all properties within a schema optional.

By default, the Schema.partial operation adds a union with undefined to the types. If you wish to avoid this, you can opt-out by passing a { exact: true } argument to the Schema.partialWith operation.

Example

ts
import { Schema } from "@effect/schema"
 
// Schema<{ readonly a?: string | undefined; }>
const schema = Schema.partial(Schema.Struct({ a: Schema.String }))
 
Schema.decodeUnknownSync(schema)({ a: "a" }) // ok
Schema.decodeUnknownSync(schema)({ a: undefined }) // ok
 
// Schema<{ readonly a?: string; }>
const exactSchema = Schema.partialWith(Schema.Struct({ a: Schema.String }), {
exact: true
})
 
Schema.decodeUnknownSync(exactSchema)({ a: "a" }) // ok
Schema.decodeUnknownSync(exactSchema)({ a: undefined })
/*
throws:
ParseError: { readonly a?: string }
└─ ["a"]
└─ Expected string, actual undefined
*/
ts
import { Schema } from "@effect/schema"
 
// Schema<{ readonly a?: string | undefined; }>
const schema = Schema.partial(Schema.Struct({ a: Schema.String }))
 
Schema.decodeUnknownSync(schema)({ a: "a" }) // ok
Schema.decodeUnknownSync(schema)({ a: undefined }) // ok
 
// Schema<{ readonly a?: string; }>
const exactSchema = Schema.partialWith(Schema.Struct({ a: Schema.String }), {
exact: true
})
 
Schema.decodeUnknownSync(exactSchema)({ a: "a" }) // ok
Schema.decodeUnknownSync(exactSchema)({ a: undefined })
/*
throws:
ParseError: { readonly a?: string }
└─ ["a"]
└─ Expected string, actual undefined
*/

required

The Schema.required operation ensures that all properties in a schema are mandatory.

ts
import { Schema } from "@effect/schema"
 
// Schema<{ readonly a: string; readonly b: number; }>
const schema = Schema.required(
Schema.Struct({
a: Schema.optionalWith(Schema.String, { exact: true }),
b: Schema.optionalWith(Schema.Number, { exact: true })
})
)
ts
import { Schema } from "@effect/schema"
 
// Schema<{ readonly a: string; readonly b: number; }>
const schema = Schema.required(
Schema.Struct({
a: Schema.optionalWith(Schema.String, { exact: true }),
b: Schema.optionalWith(Schema.Number, { exact: true })
})
)

Extending Schemas

Spreading Struct fields

Structs expose their fields through a fields property. This feature can be utilized to extend an existing struct with additional fields or to merge fields from another struct.

Example: Adding Fields

ts
import { Schema } from "@effect/schema"
 
const Struct1 = Schema.Struct({
a: Schema.String,
b: Schema.String
})
 
const Extended = Schema.Struct({
...Struct1.fields,
// other fields
c: Schema.String,
d: Schema.String
})
ts
import { Schema } from "@effect/schema"
 
const Struct1 = Schema.Struct({
a: Schema.String,
b: Schema.String
})
 
const Extended = Schema.Struct({
...Struct1.fields,
// other fields
c: Schema.String,
d: Schema.String
})

Example: Integrating Additional Index Signatures

ts
import { Schema } from "@effect/schema"
 
const Struct = Schema.Struct({
a: Schema.String,
b: Schema.String
})
 
const Extended = Schema.Struct(
Struct.fields,
Schema.Record({ key: Schema.String, value: Schema.String })
)
ts
import { Schema } from "@effect/schema"
 
const Struct = Schema.Struct({
a: Schema.String,
b: Schema.String
})
 
const Extended = Schema.Struct(
Struct.fields,
Schema.Record({ key: Schema.String, value: Schema.String })
)

Example: Merging Fields from Two Structs

ts
import { Schema } from "@effect/schema"
 
const Struct1 = Schema.Struct({
a: Schema.String,
b: Schema.String
})
 
const Struct2 = Schema.Struct({
c: Schema.String,
d: Schema.String
})
 
const Extended = Schema.Struct({
...Struct1.fields,
...Struct2.fields
})
ts
import { Schema } from "@effect/schema"
 
const Struct1 = Schema.Struct({
a: Schema.String,
b: Schema.String
})
 
const Struct2 = Schema.Struct({
c: Schema.String,
d: Schema.String
})
 
const Extended = Schema.Struct({
...Struct1.fields,
...Struct2.fields
})

The extend combinator

The Schema.extend combinator offers a structured way to extend schemas, particularly useful when direct field spreading is insufficient—for instance, when you need to extend a struct with a union of structs.

Note that not all extensions are supported, and their support depends on the nature of the involved schemas:

Possible extensions include:

  • Schema.String with another Schema.String refinement or a string literal
  • Schema.Number with another Schema.Number refinement or a number literal
  • Schema.Boolean with another Schema.Boolean refinement or a boolean literal
  • A struct with another struct where overlapping fields support extension
  • A struct with in index signature
  • A struct with a union of supported schemas
  • A refinement of a struct with a supported schema
  • A suspend of a struct with a supported schema

Example: Extending a Struct with a Union of Structs

ts
import { Schema } from "@effect/schema"
 
const Struct = Schema.Struct({
a: Schema.String
})
 
const UnionOfStructs = Schema.Union(
Schema.Struct({ b: Schema.String }),
Schema.Struct({ c: Schema.String })
)
 
const Extended = Schema.extend(Struct, UnionOfStructs)
ts
import { Schema } from "@effect/schema"
 
const Struct = Schema.Struct({
a: Schema.String
})
 
const UnionOfStructs = Schema.Union(
Schema.Struct({ b: Schema.String }),
Schema.Struct({ c: Schema.String })
)
 
const Extended = Schema.extend(Struct, UnionOfStructs)

This example shows an attempt to extend a struct with another struct where field names overlap, leading to an error:

ts
import { Schema } from "@effect/schema"
 
const Struct = Schema.Struct({
a: Schema.String
})
 
const OverlappingUnion = Schema.Union(
Schema.Struct({ a: Schema.Number }), // duplicate key
Schema.Struct({ d: Schema.String })
)
 
const Extended = Schema.extend(Struct, OverlappingUnion)
/*
throws:
Error: Unsupported schema or overlapping types
at path: ["a"]
details: cannot extend string with number
*/
ts
import { Schema } from "@effect/schema"
 
const Struct = Schema.Struct({
a: Schema.String
})
 
const OverlappingUnion = Schema.Union(
Schema.Struct({ a: Schema.Number }), // duplicate key
Schema.Struct({ d: Schema.String })
)
 
const Extended = Schema.extend(Struct, OverlappingUnion)
/*
throws:
Error: Unsupported schema or overlapping types
at path: ["a"]
details: cannot extend string with number
*/

Example: Extending a refinement of Schema.String with another refinement

ts
import { Schema } from "@effect/schema"
 
const Integer = Schema.Int.pipe(Schema.brand("Int"))
const Positive = Schema.Positive.pipe(Schema.brand("Positive"))
 
// Schema.Schema<number & Brand<"Positive"> & Brand<"Int">, number, never>
const PositiveInteger = Schema.asSchema(Schema.extend(Positive, Integer))
 
Schema.decodeUnknownSync(PositiveInteger)(-1)
/*
throws
ParseError: Int & Brand<"Int">
└─ From side refinement failure
└─ Positive & Brand<"Positive">
└─ Predicate refinement failure
└─ Expected Positive & Brand<"Positive">, actual -1
*/
 
Schema.decodeUnknownSync(PositiveInteger)(1.1)
/*
throws
ParseError: Int & Brand<"Int">
└─ Predicate refinement failure
└─ Expected Int & Brand<"Int">, actual 1.1
*/
ts
import { Schema } from "@effect/schema"
 
const Integer = Schema.Int.pipe(Schema.brand("Int"))
const Positive = Schema.Positive.pipe(Schema.brand("Positive"))
 
// Schema.Schema<number & Brand<"Positive"> & Brand<"Int">, number, never>
const PositiveInteger = Schema.asSchema(Schema.extend(Positive, Integer))
 
Schema.decodeUnknownSync(PositiveInteger)(-1)
/*
throws
ParseError: Int & Brand<"Int">
└─ From side refinement failure
└─ Positive & Brand<"Positive">
└─ Predicate refinement failure
└─ Expected Positive & Brand<"Positive">, actual -1
*/
 
Schema.decodeUnknownSync(PositiveInteger)(1.1)
/*
throws
ParseError: Int & Brand<"Int">
└─ Predicate refinement failure
└─ Expected Int & Brand<"Int">, actual 1.1
*/

Composition

Combining and reusing schemas is a common requirement, the Schema.compose combinator allows you to do just that. It enables you to combine two schemas, Schema<B, A, R1> and Schema<C, B, R2>, into a single schema Schema<C, A, R1 | R2>:

ts
import { Schema } from "@effect/schema"
 
// Schema<readonly string[], string>
const schema1 = Schema.split(",")
 
// Schema<readonly number[], readonly string[]>
const schema2 = Schema.Array(Schema.NumberFromString)
 
// Schema<readonly number[], string>
const ComposedSchema = Schema.compose(schema1, schema2)
ts
import { Schema } from "@effect/schema"
 
// Schema<readonly string[], string>
const schema1 = Schema.split(",")
 
// Schema<readonly number[], readonly string[]>
const schema2 = Schema.Array(Schema.NumberFromString)
 
// Schema<readonly number[], string>
const ComposedSchema = Schema.compose(schema1, schema2)

In this example, we have two schemas, schema1 and schema2. The first schema, schema1, takes a string and splits it into an array using a comma as the delimiter. The second schema, schema2, transforms an array of strings into an array of numbers.

Now, by using the compose combinator, we can create a new schema, ComposedSchema, that combines the functionality of both schema1 and schema2. This allows us to parse a string and directly obtain an array of numbers as a result.

Non-strict Option

If you need to be less restrictive when composing your schemas, i.e., when you have something like Schema<R1, A, B> and Schema<R2, C, D> where C is different from B, you can make use of the { strict: false } option:

ts
import { Schema } from "@effect/schema"
 
Schema.compose(
// @ts-expect-error
Schema.Union(Schema.Null, Schema.Literal("0")),
Schema.NumberFromString
)
 
// ok
Schema.compose(
Schema.Union(Schema.Null, Schema.Literal("0")),
Schema.NumberFromString,
{ strict: false }
)
ts
import { Schema } from "@effect/schema"
 
Schema.compose(
// @ts-expect-error
Schema.Union(Schema.Null, Schema.Literal("0")),
Schema.NumberFromString
)
 
// ok
Schema.compose(
Schema.Union(Schema.Null, Schema.Literal("0")),
Schema.NumberFromString,
{ strict: false }
)

Declaring New Data Types

Creating schemas for new data types is crucial to defining the expected structure of information in your application. This guide explores how to declare schemas for new data types. We'll cover two important concepts: declaring schemas for primitive data types and type constructors.

Declaring Schemas for Primitive Data Types

A primitive data type represents simple values. To declare a schema for a primitive data type, like the File type in TypeScript, we use the S.declare constructor along with a type guard. Let's go through an example:

ts
import { Schema } from "@effect/schema"
 
const FileFromSelf = Schema.declare(
(input: unknown): input is File => input instanceof File
)
 
const decode = Schema.decodeUnknownSync(FileFromSelf)
 
console.log(decode(new File([], "")))
// Output: File { size: 0, type: '', name: '', lastModified: 1724774163056 }
 
decode(null)
/*
throws
ParseError: Expected <declaration schema>, actual null
*/
ts
import { Schema } from "@effect/schema"
 
const FileFromSelf = Schema.declare(
(input: unknown): input is File => input instanceof File
)
 
const decode = Schema.decodeUnknownSync(FileFromSelf)
 
console.log(decode(new File([], "")))
// Output: File { size: 0, type: '', name: '', lastModified: 1724774163056 }
 
decode(null)
/*
throws
ParseError: Expected <declaration schema>, actual null
*/

As you can see, the error message describes what went wrong but doesn't provide much information about which schema caused the error ("Expected <declaration schema>"). To enhance the default error message, you can add annotations, particularly the identifier, title, and description annotations (none of these annotations are required, but they are encouraged for good practice and can make your schema "self-documenting"). These annotations will be utilized by the messaging system to return more meaningful messages.

A "title" should be concise, while a "description" provides a more detailed explanation of the purpose of the data described by the schema.

ts
import { Schema } from "@effect/schema"
 
const FileFromSelf = Schema.declare(
(input: unknown): input is File => input instanceof File,
{
identifier: "FileFromSelf",
description: "The `File` type in JavaScript"
}
)
 
const decode = Schema.decodeUnknownSync(FileFromSelf)
 
console.log(decode(new File([], "")))
// Output: File { size: 0, type: '', name: '', lastModified: 1724774221857 }
 
decode(null)
/*
throws
ParseError: Expected FileFromSelf, actual null
*/
ts
import { Schema } from "@effect/schema"
 
const FileFromSelf = Schema.declare(
(input: unknown): input is File => input instanceof File,
{
identifier: "FileFromSelf",
description: "The `File` type in JavaScript"
}
)
 
const decode = Schema.decodeUnknownSync(FileFromSelf)
 
console.log(decode(new File([], "")))
// Output: File { size: 0, type: '', name: '', lastModified: 1724774221857 }
 
decode(null)
/*
throws
ParseError: Expected FileFromSelf, actual null
*/

Declaring Schemas for Type Constructors

Type constructors are generic types that take one or more types as arguments and return a new type. If you need to define a schema for a type constructor, you can use the S.declare constructor. Let's illustrate this with a schema for ReadonlySet<A>:

ts
import { ParseResult, Schema } from "@effect/schema"
 
export const MyReadonlySet = <A, I, R>(
// Schema for the elements of the Set
item: Schema.Schema<A, I, R>
): Schema.Schema<ReadonlySet<A>, ReadonlySet<I>, R> =>
Schema.declare(
// Store the schema for the elements
[item],
{
// Decoding function
decode: (item) => (input, parseOptions, ast) => {
if (input instanceof Set) {
// Decode the elements
const elements = ParseResult.decodeUnknown(Schema.Array(item))(
Array.from(input.values()),
parseOptions
)
// Return a Set containing the parsed elements
return ParseResult.map(
elements,
(as): ReadonlySet<A> => new Set(as)
)
}
return ParseResult.fail(new ParseResult.Type(ast, input))
},
// Encoding function
encode: (item) => (input, parseOptions, ast) => {
if (input instanceof Set) {
// Encode the elements
const elements = ParseResult.encodeUnknown(Schema.Array(item))(
Array.from(input.values()),
parseOptions
)
// Return a Set containing the parsed elements
return ParseResult.map(
elements,
(is): ReadonlySet<I> => new Set(is)
)
}
return ParseResult.fail(new ParseResult.Type(ast, input))
}
},
{
description: `ReadonlySet<${Schema.format(item)}>`
}
)
 
// const setOfNumbers: S.Schema<ReadonlySet<string>, ReadonlySet<number>>
const setOfNumbers = MyReadonlySet(Schema.NumberFromString)
 
const decode = Schema.decodeUnknownSync(setOfNumbers)
 
console.log(decode(new Set(["1", "2", "3"]))) // Set(3) { 1, 2, 3 }
 
decode(null)
/*
throws
ParseError: Expected ReadonlySet<NumberFromString>, actual null
*/
 
decode(new Set(["1", null, "3"]))
/*
throws
ParseError: ReadonlyArray<NumberFromString>
└─ [1]
└─ NumberFromString
└─ Encoded side transformation failure
└─ Expected string, actual null
*/
ts
import { ParseResult, Schema } from "@effect/schema"
 
export const MyReadonlySet = <A, I, R>(
// Schema for the elements of the Set
item: Schema.Schema<A, I, R>
): Schema.Schema<ReadonlySet<A>, ReadonlySet<I>, R> =>
Schema.declare(
// Store the schema for the elements
[item],
{
// Decoding function
decode: (item) => (input, parseOptions, ast) => {
if (input instanceof Set) {
// Decode the elements
const elements = ParseResult.decodeUnknown(Schema.Array(item))(
Array.from(input.values()),
parseOptions
)
// Return a Set containing the parsed elements
return ParseResult.map(
elements,
(as): ReadonlySet<A> => new Set(as)
)
}
return ParseResult.fail(new ParseResult.Type(ast, input))
},
// Encoding function
encode: (item) => (input, parseOptions, ast) => {
if (input instanceof Set) {
// Encode the elements
const elements = ParseResult.encodeUnknown(Schema.Array(item))(
Array.from(input.values()),
parseOptions
)
// Return a Set containing the parsed elements
return ParseResult.map(
elements,
(is): ReadonlySet<I> => new Set(is)
)
}
return ParseResult.fail(new ParseResult.Type(ast, input))
}
},
{
description: `ReadonlySet<${Schema.format(item)}>`
}
)
 
// const setOfNumbers: S.Schema<ReadonlySet<string>, ReadonlySet<number>>
const setOfNumbers = MyReadonlySet(Schema.NumberFromString)
 
const decode = Schema.decodeUnknownSync(setOfNumbers)
 
console.log(decode(new Set(["1", "2", "3"]))) // Set(3) { 1, 2, 3 }
 
decode(null)
/*
throws
ParseError: Expected ReadonlySet<NumberFromString>, actual null
*/
 
decode(new Set(["1", null, "3"]))
/*
throws
ParseError: ReadonlyArray<NumberFromString>
└─ [1]
└─ NumberFromString
└─ Encoded side transformation failure
└─ Expected string, actual null
*/

The decoding and encoding functions cannot use context (the R type parameter) and cannot use async effects.

Adding Annotations

When you define a new data type, some compilers like Arbitrary or Pretty may not know how to handle the newly defined data. For instance:

ts
import { Arbitrary, Schema } from "@effect/schema"
 
const FileFromSelf = Schema.declare(
(input: unknown): input is File => input instanceof File,
{
identifier: "FileFromSelf"
}
)
 
// Create an Arbitrary instance for FileFromSelf schema
const arb = Arbitrary.make(FileFromSelf)
/*
throws:
Error: Missing annotation
details: Generating an Arbitrary for this schema requires an "arbitrary" annotation
schema (Declaration): FileFromSelf
*/
ts
import { Arbitrary, Schema } from "@effect/schema"
 
const FileFromSelf = Schema.declare(
(input: unknown): input is File => input instanceof File,
{
identifier: "FileFromSelf"
}
)
 
// Create an Arbitrary instance for FileFromSelf schema
const arb = Arbitrary.make(FileFromSelf)
/*
throws:
Error: Missing annotation
details: Generating an Arbitrary for this schema requires an "arbitrary" annotation
schema (Declaration): FileFromSelf
*/

In such cases, you need to provide annotations to ensure proper functionality:

ts
import { Arbitrary, FastCheck, Pretty, Schema } from "@effect/schema"
 
const FileFromSelf = Schema.declare(
(input: unknown): input is File => input instanceof File,
{
identifier: "FileFromSelf",
// Provide an arbitrary function to generate random File instances
arbitrary: () => (fc) =>
fc
.tuple(fc.string(), fc.string())
.map(([content, path]) => new File([content], path)),
// Provide a pretty function to generate human-readable representation of File instances
pretty: () => (file) => `File(${file.name})`
}
)
 
// Create an Arbitrary instance for FileFromSelf schema
const arb = Arbitrary.make(FileFromSelf)
 
// Generate sample files using the Arbitrary instance
const files = FastCheck.sample(arb, 2)
console.log(files)
/*
Example Output:
[
File { size: 5, type: '', name: 'C', lastModified: 1706435571176 },
File { size: 1, type: '', name: '98Ggmc', lastModified: 1706435571176 }
]
*/
 
// Create a Pretty instance for FileFromSelf schema
const pretty = Pretty.make(FileFromSelf)
 
// Print human-readable representation of a file
console.log(pretty(files[0]))
// Example Output: "File(C)"
ts
import { Arbitrary, FastCheck, Pretty, Schema } from "@effect/schema"
 
const FileFromSelf = Schema.declare(
(input: unknown): input is File => input instanceof File,
{
identifier: "FileFromSelf",
// Provide an arbitrary function to generate random File instances
arbitrary: () => (fc) =>
fc
.tuple(fc.string(), fc.string())
.map(([content, path]) => new File([content], path)),
// Provide a pretty function to generate human-readable representation of File instances
pretty: () => (file) => `File(${file.name})`
}
)
 
// Create an Arbitrary instance for FileFromSelf schema
const arb = Arbitrary.make(FileFromSelf)
 
// Generate sample files using the Arbitrary instance
const files = FastCheck.sample(arb, 2)
console.log(files)
/*
Example Output:
[
File { size: 5, type: '', name: 'C', lastModified: 1706435571176 },
File { size: 1, type: '', name: '98Ggmc', lastModified: 1706435571176 }
]
*/
 
// Create a Pretty instance for FileFromSelf schema
const pretty = Pretty.make(FileFromSelf)
 
// Print human-readable representation of a file
console.log(pretty(files[0]))
// Example Output: "File(C)"

Recursive Schemas

The Schema.suspend function is useful when you need to define a schema that depends on itself, like in the case of recursive data structures.

Example

In this example, the Category schema depends on itself because it has a field subcategories that is an array of Category objects.

ts
import { Schema } from "@effect/schema"
 
interface Category {
readonly name: string
readonly subcategories: ReadonlyArray<Category>
}
 
const Category = Schema.Struct({
name: Schema.String,
subcategories: Schema.Array(
Schema.suspend((): Schema.Schema<Category> => Category)
)
})
ts
import { Schema } from "@effect/schema"
 
interface Category {
readonly name: string
readonly subcategories: ReadonlyArray<Category>
}
 
const Category = Schema.Struct({
name: Schema.String,
subcategories: Schema.Array(
Schema.suspend((): Schema.Schema<Category> => Category)
)
})

It is necessary to define the Category type and add an explicit type annotation because otherwise TypeScript would struggle to infer types correctly. Without this annotation, you might encounter the error message:

ts
import { Schema } from "@effect/schema"
 
const Category = Schema.Struct({
'Category' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.7022'Category' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.
name: Schema.String,
subcategories: Schema.Array(Schema.suspend(() => Category))
Function implicitly has return type 'any' because it does not have a return type annotation and is referenced directly or indirectly in one of its return expressions.7024Function implicitly has return type 'any' because it does not have a return type annotation and is referenced directly or indirectly in one of its return expressions.
})
ts
import { Schema } from "@effect/schema"
 
const Category = Schema.Struct({
'Category' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.7022'Category' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.
name: Schema.String,
subcategories: Schema.Array(Schema.suspend(() => Category))
Function implicitly has return type 'any' because it does not have a return type annotation and is referenced directly or indirectly in one of its return expressions.7024Function implicitly has return type 'any' because it does not have a return type annotation and is referenced directly or indirectly in one of its return expressions.
})

A Helpful Pattern to Simplify Schema Definition

As we've observed, it's necessary to define an interface for the Type of the schema to enable recursive schema definition, which can complicate things and be quite tedious. One pattern to mitigate this is to separate the field responsible for recursion from all other fields.

ts
import { Schema } from "@effect/schema"
 
const fields = {
name: Schema.String
// ...possibly other fields
}
 
// Define an interface for the Category schema, extending the Type of the defined fields
interface Category extends Schema.Struct.Type<typeof fields> {
// Define `subcategories` using recursion
readonly subcategories: ReadonlyArray<Category>
}
 
const Category = Schema.Struct({
...fields, // Include the fields
subcategories: Schema.Array(
// Define `subcategories` using recursion
Schema.suspend((): Schema.Schema<Category> => Category)
)
})
ts
import { Schema } from "@effect/schema"
 
const fields = {
name: Schema.String
// ...possibly other fields
}
 
// Define an interface for the Category schema, extending the Type of the defined fields
interface Category extends Schema.Struct.Type<typeof fields> {
// Define `subcategories` using recursion
readonly subcategories: ReadonlyArray<Category>
}
 
const Category = Schema.Struct({
...fields, // Include the fields
subcategories: Schema.Array(
// Define `subcategories` using recursion
Schema.suspend((): Schema.Schema<Category> => Category)
)
})

Mutually Recursive Schemas

Here's an example of two mutually recursive schemas, Expression and Operation, that represent a simple arithmetic expression tree.

ts
import { Schema } from "@effect/schema"
 
interface Expression {
readonly type: "expression"
readonly value: number | Operation
}
 
interface Operation {
readonly type: "operation"
readonly operator: "+" | "-"
readonly left: Expression
readonly right: Expression
}
 
const Expression = Schema.Struct({
type: Schema.Literal("expression"),
value: Schema.Union(
Schema.Number,
Schema.suspend((): Schema.Schema<Operation> => Operation)
)
})
 
const Operation = Schema.Struct({
type: Schema.Literal("operation"),
operator: Schema.Literal("+", "-"),
left: Expression,
right: Expression
})
ts
import { Schema } from "@effect/schema"
 
interface Expression {
readonly type: "expression"
readonly value: number | Operation
}
 
interface Operation {
readonly type: "operation"
readonly operator: "+" | "-"
readonly left: Expression
readonly right: Expression
}
 
const Expression = Schema.Struct({
type: Schema.Literal("expression"),
value: Schema.Union(
Schema.Number,
Schema.suspend((): Schema.Schema<Operation> => Operation)
)
})
 
const Operation = Schema.Struct({
type: Schema.Literal("operation"),
operator: Schema.Literal("+", "-"),
left: Expression,
right: Expression
})

Recursive Types with Different Encoded and Type

Defining a recursive schema where the Encoded type differs from the Type type adds another layer of complexity. In such cases, we need to define two interfaces: one for the Type type, as seen previously, and another for the Encoded type.

Example

Let's consider an example: suppose we want to add an id field to the Category schema, where the schema for id is NumberFromString. It's important to note that NumberFromString is a schema that transforms a string into a number, so the Type and Encoded types of NumberFromString differ, being number and string respectively. When we add this field to the Category schema, TypeScript raises an error:

ts
import { Schema } from "@effect/schema"
 
const fields = {
id: Schema.NumberFromString,
name: Schema.String
}
 
interface Category extends Schema.Struct.Type<typeof fields> {
readonly subcategories: ReadonlyArray<Category>
}
 
const Category = Schema.Struct({
...fields,
subcategories: Schema.Array(
Schema.suspend((): Schema.Schema<Category> => Category)
Type 'Struct<{ subcategories: Array$<suspend<Category, Category, never>>; id: typeof NumberFromString; name: typeof String$; }>' is not assignable to type 'Schema<Category, Category, never>'. The types of 'Encoded.id' are incompatible between these types. Type 'string' is not assignable to type 'number'.2322Type 'Struct<{ subcategories: Array$<suspend<Category, Category, never>>; id: typeof NumberFromString; name: typeof String$; }>' is not assignable to type 'Schema<Category, Category, never>'. The types of 'Encoded.id' are incompatible between these types. Type 'string' is not assignable to type 'number'.
)
})
ts
import { Schema } from "@effect/schema"
 
const fields = {
id: Schema.NumberFromString,
name: Schema.String
}
 
interface Category extends Schema.Struct.Type<typeof fields> {
readonly subcategories: ReadonlyArray<Category>
}
 
const Category = Schema.Struct({
...fields,
subcategories: Schema.Array(
Schema.suspend((): Schema.Schema<Category> => Category)
Type 'Struct<{ subcategories: Array$<suspend<Category, Category, never>>; id: typeof NumberFromString; name: typeof String$; }>' is not assignable to type 'Schema<Category, Category, never>'. The types of 'Encoded.id' are incompatible between these types. Type 'string' is not assignable to type 'number'.2322Type 'Struct<{ subcategories: Array$<suspend<Category, Category, never>>; id: typeof NumberFromString; name: typeof String$; }>' is not assignable to type 'Schema<Category, Category, never>'. The types of 'Encoded.id' are incompatible between these types. Type 'string' is not assignable to type 'number'.
)
})

This error occurs because the explicit annotation Schema.Schema<Category> is no longer sufficient and needs to be adjusted by explicitly adding the Encoded type:

ts
import { Schema } from "@effect/schema"
 
const fields = {
id: Schema.NumberFromString,
name: Schema.String
}
 
interface Category extends Schema.Struct.Type<typeof fields> {
readonly subcategories: ReadonlyArray<Category>
}
 
interface CategoryEncoded extends Schema.Struct.Encoded<typeof fields> {
readonly subcategories: ReadonlyArray<CategoryEncoded>
}
 
const Category = Schema.Struct({
...fields,
subcategories: Schema.Array(
Schema.suspend((): Schema.Schema<Category, CategoryEncoded> => Category)
)
})
ts
import { Schema } from "@effect/schema"
 
const fields = {
id: Schema.NumberFromString,
name: Schema.String
}
 
interface Category extends Schema.Struct.Type<typeof fields> {
readonly subcategories: ReadonlyArray<Category>
}
 
interface CategoryEncoded extends Schema.Struct.Encoded<typeof fields> {
readonly subcategories: ReadonlyArray<CategoryEncoded>
}
 
const Category = Schema.Struct({
...fields,
subcategories: Schema.Array(
Schema.suspend((): Schema.Schema<Category, CategoryEncoded> => Category)
)
})