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
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
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.
Literals
Literal schemas represent a literal type.
You can use them to specify exact values that a type must have.
Union of Literals
You can create a union of multiple literals by passing them as arguments to the Schema.Literal constructor:
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
Sometimes, you may need to reuse a literal schema in other parts of your code. Below is an example demonstrating how to do this:
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
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
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
Union of Literals
While you can create a union of literals by combining individual literal schemas:
You can simplify the process by passing multiple literals directly to the Schema.Literal constructor:
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 provides utility functions to create schemas for nullable types:
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:
This code defines a discriminated union using the Schema module:
In this example, the Schema.Literal constructor is used to define the discriminant property (kind) for each type. The Shape schema represents a discriminated union of Circle and Square.
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
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:
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:
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
Append a Required Element
You can append additional required elements to an existing tuple by using the spread operator:
Example
Optional Elements
To define an optional element, use the Schema.optionalElement constructor.
Example
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
You can also include other elements after the rest:
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
Exposed Values
You can access the elements and rest elements of a tuple schema using the elements and rest properties:
Arrays
The Schema module allows you to define schemas for arrays, making it easy to validate collections of elements of a specific type.
Example
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
Exposed Values
You can access the value type of an array schema using the value property:
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
Exposed Values
You can access the value type of a non-empty array schema using the value property:
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.
Defining a Record with String Keys
Defining a Record with Symbol Keys
Defining a Record with Union of Literal Keys
Defining a Record with Template Literal Keys
Defining a Record with Refined Keys
Creating 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.
Exposed Values
You can access the key and value types of a record schema using the key and value properties:
Structs
The Schema.Struct constructor allows you to define a schema for an object with specific properties.
Example
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
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:
Mutable Properties
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
Exposed Values
You can access the fields and records of a struct schema using the fields and records properties:
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
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
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:
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
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:
In such cases, you cannot use Schema.instanceOf, and you must rely on Schema.declare like this:
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
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 (Creating an Exact Partial Schema)
required
The Schema.required function ensures that all properties in a schema are mandatory.
Example
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.