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:
In this example, if you input 2, the schema will decode it to 4 and encode it back to 2.
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 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
Sometimes the strict type checking can impede certain operations where types might slightly deviate during the transformation process. For such cases, transform provides an option, strict: false, to relax type constraints and allow for more flexible data manipulation.
Example (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:
Type
Missing
Unexpected
Forbidden
Pointer
Refinement
Transformation
Composite
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.
In this example:
Decoding: Attempts to parse the input string into a number. If the parsing results in NaN (indicating that the string is not a valid number), it fails with a descriptive error.
Encoding: Converts the number back to a string, assuming that the input number is valid.
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 (Asynchronously Converting a String to a Number Using an API)
Consider a situation where you need to validate a person’s ID by fetching data from an external API. Here’s how you can implement it:
Declaring Dependencies
For more complex scenarios where your transformation might depend on external services like a fetching function, you can declare these dependencies explicitly.
Example (Injecting Dependencies)
Here’s how to inject a fetch dependency into your transformation process:
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>:
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:
Effectful Filters
The Schema.filterEffect function enhances the Schema.filter functionality by allowing the integration of effects, thus enabling asynchronous or dynamic validation scenarios. This is particularly useful when validations need to perform operations that require side effects, such as network requests or database queries.
Example (Validating Usernames Asynchronously)
String Transformations
split
Splits a string into an array of strings.
Trim
Removes whitespaces from the beginning and end of a string.
Note. If you were looking for a combinator to check if a string is trimmed, check out the trimmed filter.
Lowercase
Converts a string to lowercase.
Uppercase
Converts a string to uppercase.
Capitalize
Converts a string to capitalized one.
Uncapitalize
Converts a string to uncapitalized one.
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.
Additionally, you can refine the parsing result by providing a schema to the parseJson constructor:
In this example, we’ve used parseJson with a struct schema to ensure that the parsed result has a specific structure, including an object with a numeric property “a”. This helps in handling JSON data with predefined shapes.
StringFromBase64
Decodes a base64 (RFC4648) encoded string into a UTF-8 string.
StringFromBase64Url
Decodes a base64 (URL) encoded string into a UTF-8 string.
StringFromHex
Decodes a hex encoded string into a UTF-8 string.
Number Transformations
NumberFromString
Transforms a string into a number by parsing the string using parseFloat.
The following special string values are supported: “NaN”, “Infinity”, “-Infinity”.
clamp
Clamps a number between a minimum and a maximum value.
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”.
Boolean Transformations
Not
Negates a boolean value.
Symbol transformations
Symbol
Transforms a string into a symbol by parsing the string using Symbol.for.
BigInt transformations
BigInt
Transforms a string into a BigInt by parsing the string using the BigInt constructor.
BigIntFromNumber
Transforms a number into a BigInt by parsing the number using the BigInt constructor.
clampBigInt
Clamps a BigInt between a minimum and a maximum value.
Date transformations
Date
Transforms a string into a validDate, ensuring that invalid dates, such as new Date("Invalid Date"), are rejected.
BigDecimal Transformations
BigDecimal
Transforms a string into a BigDecimal.
BigDecimalFromNumber
Transforms a number into a BigDecimal.
clampBigDecimal
Clamps a BigDecimal between a minimum and a maximum value.