The Schema module provides built-in schemas for common primitive types.
Schema
Equivalent TypeScript Type
Schema.String
string
Schema.Number
number
Schema.Boolean
boolean
Schema.BigIntFromSelf
BigInt
Schema.SymbolFromSelf
symbol
Schema.Object
object
Schema.Undefined
undefined
Schema.Void
void
Schema.Any
any
Schema.Unknown
unknown
Schema.Never
never
Example (Using a Primitive Schema)
asSchema
To make it easier to work with schemas, built-in schemas are exposed with shorter, opaque types when possible.
The Schema.asSchema function allows you to view any schema as Schema<Type, Encoded, Context>.
Example (Expanding a Schema with asSchema)
For example, while Schema.String is defined as a class with a type of typeof Schema.String, using Schema.asSchema provides the schema in its extended form as Schema<string, string, never>.
Unique Symbols
You can create a schema for unique symbols using Schema.UniqueSymbolFromSelf.
Example (Creating a Schema for a Unique Symbol)
Literals
Literal schemas represent a literal type.
You can use them to specify exact values that a type must have.
Literals can be of the following types:
string
number
boolean
null
bigint
Example (Defining Literal Schemas)
Example (Defining a Literal Schema for "a")
Union of Literals
You can create a union of multiple literals by passing them as arguments to the Schema.Literal constructor:
Example (Defining a Union of Literals)
If you want to set a custom error message for the entire union of literals, you can use the override: true option (see Custom Error Messages for more details) to specify a unified message.
Example (Adding a Custom Message to a Union of Literals)
Exposed Values
You can access the literals defined in a literal schema using the literals property:
The pickLiteral Utility
You can use Schema.pickLiteral with a literal schema to narrow down its possible values.
Example (Using pickLiteral to Narrow Values)
Sometimes, you may need to reuse a literal schema in other parts of your code. Below is an example demonstrating how to do this:
Example (Creating a Subtype from a Literal Schema)
In this example, FruitCategory serves as the source of truth for the different fruit categories.
We reuse it to create a subtype of Fruit called SweetAndCitrusFruit, ensuring that only the specified categories ("sweet" and "citrus") are allowed.
This approach helps maintain consistency throughout your code and provides type safety if the category definition changes.
Template literals
In TypeScript, template literals types allow you to embed expressions within string literals.
The Schema.TemplateLiteral constructor allows you to create a schema for these template literal types.
Let’s look at a more complex example. Suppose you have two sets of locale IDs for emails and footers.
You can use the Schema.TemplateLiteral constructor to create a schema that combines these IDs:
Supported Span Types
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 Schema.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.
Example (Using TemplateLiteralParser for Parsing and Encoding)
Native enums
The Schema module provides support for native TypeScript enums. You can define a schema for an enum using Schema.Enums, allowing you to validate values that belong to the enum.
Example (Defining a Schema for an Enum)
Exposed Values
Enums are accessible through the enums property of the schema. You can use this property to retrieve individual members or the entire set of enum values.
Unions
The Schema module includes a built-in Schema.Union constructor for creating “OR” types, allowing you to define schemas that can represent multiple types.
Example (Defining a Union Schema)
Union of Literals
While you can create a union of literals by combining individual literal schemas:
Example (Using Individual Literal Schemas)
You can simplify the process by passing multiple literals directly to the Schema.Literal constructor:
Example (Defining a Union of Literals)
If you want to set a custom error message for the entire union of literals, you can use the override: true option (see Custom Error Messages for more details) to specify a unified message.
Example (Adding a Custom Message to a Union of Literals)
Nullables
The Schema module includes utility functions for defining schemas that allow nullable types, helping to handle values that may be null, undefined, or both.
Example (Creating Nullable Schemas)
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.
Example (Defining a Discriminated Union in TypeScript)
In the Schema module, you can define a discriminated union similarly by specifying a literal field as the discriminant for each type.
Example (Defining a Discriminated Union Using Schema)
In this example, the Schema.Literal constructor sets up the kind property as the discriminant for both Circle and Square schemas. The Shape schema then represents a union of these two types, allowing TypeScript to infer the specific shape based on the kind value.
Transforming a Simple Union into a Discriminated Union
If you start with a simple union and want to transform it into a discriminated union, you can add a special property to each member. This allows TypeScript to automatically infer the correct type based on the value of the discriminant property.
Example (Initial Simple Union)
For example, let’s say you’ve defined a Shape union as a combination of Circle and Square without any special property:
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:
Example (Adding Discriminant Property)
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:
Example (Using Schema.attachPropertySignature for Less Code)
Exposed Values
You can access the individual members of a union schema represented as a tuple:
Tuples
The Schema module allows you to define tuples, which are ordered collections of elements that may have different types.
You can define tuples with required, optional, or rest elements.
Required Elements
To define a tuple with required elements, you can use the Schema.Tuple constructor and simply list the element schemas in order:
Example (Defining a Tuple with Required Elements)
Append a Required Element
You can append additional required elements to an existing tuple by using the spread operator:
Example (Adding an Element to an Existing Tuple)
Optional Elements
To define an optional element, use the Schema.optionalElement constructor.
Example (Defining a Tuple with Optional Elements)
Rest Element
To define a rest element, add it after the list of required or optional elements.
The rest element allows the tuple to accept additional elements of a specific type.
Example (Using a Rest Element)
You can also include other elements after the rest:
Example (Including Additional Elements After a Rest Element)
Annotations
Annotations are useful for adding metadata to tuple elements, making it easier to describe their purpose or requirements.
This is especially helpful for generating documentation or JSON schemas.
Example (Adding Annotations to Tuple Elements)
Exposed Values
You can access the elements and rest elements of a tuple schema using the elements and rest properties:
Example (Accessing Elements and Rest Element in a Tuple Schema)
Arrays
The Schema module allows you to define schemas for arrays, making it easy to validate collections of elements of a specific type.
Example (Defining an Array Schema)
Mutable Arrays
By default, Schema.Array generates a type marked as readonly.
To create a schema for a mutable array, you can use the Schema.mutable function, which makes the array type mutable in a shallow manner.
Example (Creating a Mutable Array Schema)
Exposed Values
You can access the value type of an array schema using the value property:
Example (Accessing the Value Type of an Array Schema)
Non Empty Arrays
The Schema module also provides a way to define schemas for non-empty arrays, ensuring that the array always contains at least one element.
Example (Defining a Non-Empty Array Schema)
Exposed Values
You can access the value type of a non-empty array schema using the value property:
Example (Accessing the Value Type of a Non-Empty Array Schema)
Records
The Schema module provides support for defining record types, which are collections of key-value pairs where the key can be a string, symbol, or other types, and the value has a defined schema.
String Keys
You can define a record with string keys and a specified type for the values.
Example (String Keys with Number Values)
Symbol Keys
Records can also use symbols as keys.
Example (Symbol Keys with Number Values)
Union of Literal Keys
Use a union of literals to restrict keys to a specific set of values.
Example (Union of String Literals as Keys)
Template Literal Keys
Records can use template literals as keys, allowing for more complex key patterns.
Example (Template Literal Keys with Number Values)
Refined Keys
You can refine the key type with additional constraints.
Example (Refined Keys with Minimum Length Constraint)
Mutable Records
By default, Schema.Record generates a type marked as readonly.
To create a schema for a mutable record, you can use the Schema.mutable function, which makes the record type mutable in a shallow manner.
Example (Creating a Mutable Record Schema)
Exposed Values
You can access the key and value types of a record schema using the key and value properties:
Example (Accessing Key and Value Types)
Structs
The Schema.Struct constructor allows you to define a schema for an object with specific properties.
Example (Defining a Struct Schema)
Index Signatures
The Schema.Struct constructor can optionally accept a list of key/value pairs representing index signatures, allowing you to define additional dynamic properties.
Example (Adding an Index Signature)
Since the Schema.Record constructor returns a schema that exposes both the key and value, you can simplify the above code by using the Schema.Record constructor:
Example (Simplifying with Schema.Record)
Mutable Structs
By default, Schema.Struct generates a type with properties marked as readonly.
To create a mutable version of the struct, use the Schema.mutable function, which makes the properties mutable in a shallow manner.
Example (Creating a Mutable Struct Schema)
Exposed Values
You can access the fields and records of a struct schema using the fields and records properties:
Example (Accessing Fields and Records)
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 Schema.tag constructor is specifically designed to create a property signature that holds a specific literal value, serving as the discriminator for object types.
Example (Defining a Tagged Struct)
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 Schema.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.
Example (Using TaggedStruct for a Simplified Tagged Struct)
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:
Example (Adding Multiple Tags to a Struct)
This example defines a product schema with a primary tag ("Product") and an additional category tag ("Electronics"), adding further specificity to the data structure.
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 (Defining a Schema with instanceOf)
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:
Example (Error With Private Constructors)
In such cases, you cannot use Schema.instanceOf, and you must rely on Schema.declare like this:
Example (Using Schema.declare With Private Constructors)
Picking
The pick static function available on each struct schema can be used to create a new Struct by selecting specific properties from an existing Struct.
Example (Picking Properties from a Struct)
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 Properties from a Union)
Omitting
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.
Example (Omitting Properties from a Struct)
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.
Example (Omitting Properties from a Union)
partial
The Schema.partial function makes all properties within a schema optional.
Example (Making All Properties Optional)
By default, the Schema.partial operation adds undefined to the type of each property. If you want to avoid this, you can use Schema.partialWith and pass { exact: true } as an argument.
Example (Defining an Exact Partial Schema)
required
The Schema.required function ensures that all properties in a schema are mandatory.
Example (Making All Properties Required)
In this example, both a and b are made required, even though they were initially defined as optional.
keyof
The Schema.keyof operation creates a schema that represents the keys of a given object schema.