Transformations play a key role in working with schemas, especially when you need to convert data from one type to another, such as parsing a string into a number or converting a date string into a Date object.
transform
The Schema.transform function is designed to facilitate these conversions by linking two schemas together: one for the input type and one for the output type.
Here’s an overview of the Schema.transform function, which accepts five parameters:
Parameter
Description
Type
from
The source schema, representing the starting point of the transformation.
Schema<B, A, R1> where A is the input type and B is the intermediate type after initial validation.
to
The target schema, representing the endpoint of the transformation.
Schema<D, C, R2> where C is the transformed type from B, and D is the final output type.
decode
A function that converts an intermediate value of type B to a value of type C.
(b: B, a: A) => C
encode
A function that reverses the transformation, converting type C back to type B.
(c: C, d: D) => B
strict
optional (but recommended)
boolean
This function results in a schema Schema<D, A, R1 | R2>, integrating both the dependencies and transformations of the from and to schemas.
Example (Doubling a Number)
Here’s an example that demonstrates a schema transformation to double an input number:
Example (Converting an array to a ReadonlySet)
Here’s how you can convert an array to a ReadonlySet:
Example (Trim Whitespace)
Here’s how to use the transform function to trim whitespace from strings:
This schema automatically trims leading and trailing whitespace from a string during decoding. During encoding, it returns the string unchanged.
Improving the Transformation with a Filter
To ensure that strings are not only trimmed but also validated to exclude untrimmed inputs, you can restrict the target schema to only accept strings that are already trimmed:
In this improved example, the target schema is piped through a Schema.filter function. This function checks that the string is equal to its trimmed version, effectively ensuring that only strings without leading or trailing whitespace are considered valid. This is particularly useful for maintaining data integrity and can help prevent errors or inconsistencies in data processing.
Non-strict option
In some cases, strict type checking can create issues during data transformations, especially when the types might slightly differ in specific transformations. To address these scenarios, Schema.transform offers the option strict: false, which relaxes type constraints and allows more flexible transformations.
Example (Creating a Clamping Constructor)
Let’s consider the scenario where you need to define a constructor clamp that ensures a number falls within a specific range. This function returns a schema that “clamps” a number to a specified minimum and maximum range:
In this code, Number.clamp is a function that adjusts the given number to stay within the specified range. However, the return type of Number.clamp may not strictly be of type A but just a number, which can lead to type mismatches according to TypeScript’s strict type-checking.
There are two ways to resolve the type mismatch:
Using Type Assertion:
Adding a type cast can enforce the return type to be treated as type A:
Using the Non-Strict Option:
Setting strict: false in the transformation options allows the schema to bypass some of TypeScript’s type-checking rules, accommodating the type discrepancy:
transformOrFail
While the Schema.transform function is suitable for error-free transformations,
the Schema.transformOrFail function is designed for more complex scenarios where transformations
can fail during the decoding or encoding stages.
This function enables decoding/encoding functions to return either a successful result or an error,
making it particularly useful for validating and processing data that might not always conform to expected formats.
Error Handling
The Schema.transformOrFail function utilizes the ParseResult module to manage potential errors:
Constructor
Description
ParseResult.succeed
Indicates a successful transformation, where no errors occurred.
ParseResult.fail
Signals a failed transformation, creating a new ParseError based on the provided ParseIssue.
Additionally, the ParseResult module provides constructors for dealing with various types of parse issues, such as:
Parse Issue Type
Description
Type
Indicates a type mismatch error.
Missing
Used when a required field is missing.
Unexpected
Used for unexpected fields that are not allowed in the schema.
Forbidden
Flags the decoding or encoding operation being forbidden by the schema.
Pointer
Points to a specific location in the data where an issue occurred.
Refinement
Used when a value does not meet a specific refinement or constraint.
Transformation
Flags issues that occur during transformation from one type to another.
Composite
Represents a composite error, combining multiple issues into one, helpful for grouped errors.
These tools allow for detailed and specific error handling, enhancing the reliability of data processing operations.
Example (Converting a String to a Number)
A common use case for Schema.transformOrFail is converting string representations of numbers into actual numeric types. This scenario is typical when dealing with user inputs or data from external sources.
Both decode and encode functions not only receive the value to transform (input), but also the parse options that the user sets when using the resulting schema, and the ast, which represents the low level definition of the schema you’re transforming.
Async Transformations
In modern applications, especially those interacting with external APIs, you might need to transform data asynchronously. Schema.transformOrFail supports asynchronous transformations by allowing you to return an Effect.
Example (Validating Data with an API Call)
Consider a scenario where you need to validate a person’s ID by making an API call. Here’s how you can implement it:
Declaring Dependencies
In cases where your transformation depends on external services, you can inject these services in the decode or encode functions. These dependencies are then tracked in the Requirements channel of the schema:
Example (Validating Data with a Service)
Composition
Combining and reusing schemas is often needed in complex applications, and the Schema.compose combinator provides an efficient way to do this. With Schema.compose, you can chain two schemas, Schema<B, A, R1> and Schema<C, B, R2>, into a single schema Schema<C, A, R1 | R2>:
Example (Composing Schemas to Parse a Delimited String into Numbers)
Non-strict Option
When composing schemas, you may encounter cases where the output of one schema does not perfectly match the input of the next, for example, if you have Schema<R1, A, B> and Schema<R2, C, D> where C differs from B. To handle these cases, you can use the { strict: false } option to relax type constraints.
Example (Using Non-strict Option in Composition)
Effectful Filters
The Schema.filterEffect function enables validations that require asynchronous or dynamic scenarios, making it suitable for cases where validations involve side effects like network requests or database queries. For simple synchronous validations, see Schema.filter.
Example (Asynchronous Username Validation)
String Transformations
split
Splits a string by a specified delimiter into an array of substrings.
Example (Splitting a String by Comma)
Trim
Removes whitespace from the beginning and end of a string.
Example (Trimming Whitespace)
Lowercase
Converts a string to lowercase.
Example (Converting to Lowercase)
Uppercase
Converts a string to uppercase.
Example (Converting to Uppercase)
Capitalize
Converts the first character of a string to uppercase.
Example (Capitalizing a String)
Uncapitalize
Converts the first character of a string to lowercase.
Example (Uncapitalizing a String)
parseJson
The Schema.parseJson constructor offers a method to convert JSON strings into the unknown type using the underlying functionality of JSON.parse.
It also employs JSON.stringify for encoding.
Example (Parsing JSON Strings)
To further refine the result of JSON parsing, you can provide a schema to the Schema.parseJson constructor. This schema will validate that the parsed JSON matches a specific structure.
Example (Parsing JSON with Structured Validation)
In this example, Schema.parseJson uses a struct schema to ensure the parsed JSON is an object with a numeric property a. This adds validation to the parsed data, confirming that it follows the expected structure.
StringFromBase64
Decodes a base64 (RFC4648) encoded string into a UTF-8 string.
Example (Decoding Base64)
StringFromBase64Url
Decodes a base64 (URL) encoded string into a UTF-8 string.
Example (Decoding Base64 URL)
StringFromHex
Decodes a hex encoded string into a UTF-8 string.
Example (Decoding Hex String)
Number Transformations
NumberFromString
Converts a string to a number using parseFloat, supporting special values “NaN”, “Infinity”, and “-Infinity”.
Example (Parsing Number from String)
clamp
Restricts a number within a specified range.
Example (Clamping a Number)
parseNumber
Transforms a string into a number by parsing the string using the parse function of the effect/Number module.
It returns an error if the value can’t be converted (for example when non-numeric characters are provided).
The following special string values are supported: “NaN”, “Infinity”, “-Infinity”.
Example (Parsing and Validating Numbers)
Boolean Transformations
Not
Negates a boolean value.
Example (Negating Boolean)
Symbol transformations
Symbol
Converts a string to a symbol using Symbol.for.
Example (Creating Symbols from Strings)
BigInt transformations
BigInt
Converts a string to a BigInt using the BigInt constructor.
Example (Parsing BigInt from String)
BigIntFromNumber
Converts a number to a BigInt using the BigInt constructor.
Example (Parsing BigInt from Number)
clampBigInt
Restricts a BigInt within a specified range.
Example (Clamping BigInt)
Date transformations
Date
Converts a string into a validDate, ensuring that invalid dates, such as new Date("Invalid Date"), are rejected.