Transformations

On this page

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:

ParameterDescriptionType
fromThe 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.
toThe 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.
decodeA function that converts an intermediate value of type B to a value of type C.(b: B, a: A) => C
encodeA function that reverses the transformation, converting type C back to type B.(c: C, d: D) => B
strictoptional (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:

ts
import { Schema } from "@effect/schema"
 
// Define a transformation that doubles the input number
const transformedSchema = Schema.transform(
// Source schema
Schema.Number,
// Target schema
Schema.Number,
{
// optional but you get better error messages from TypeScript
strict: true,
// Transformation function to double the number
decode: (n) => n * 2,
// Reverse transformation to revert to the original number
encode: (n) => n / 2
}
)
ts
import { Schema } from "@effect/schema"
 
// Define a transformation that doubles the input number
const transformedSchema = Schema.transform(
// Source schema
Schema.Number,
// Target schema
Schema.Number,
{
// optional but you get better error messages from TypeScript
strict: true,
// Transformation function to double the number
decode: (n) => n * 2,
// Reverse transformation to revert to the original number
encode: (n) => n / 2
}
)

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:

ts
import { Schema } from "@effect/schema"
 
const ReadonlySetFromArray = <A, I, R>(
itemSchema: Schema.Schema<A, I, R>
): Schema.Schema<ReadonlySet<A>, ReadonlyArray<I>, R> =>
Schema.transform(
Schema.Array(itemSchema),
Schema.ReadonlySetFromSelf(Schema.typeSchema(itemSchema)),
{
strict: true,
decode: (items) => new Set(items),
encode: (set) => Array.from(set.values())
}
)
ts
import { Schema } from "@effect/schema"
 
const ReadonlySetFromArray = <A, I, R>(
itemSchema: Schema.Schema<A, I, R>
): Schema.Schema<ReadonlySet<A>, ReadonlyArray<I>, R> =>
Schema.transform(
Schema.Array(itemSchema),
Schema.ReadonlySetFromSelf(Schema.typeSchema(itemSchema)),
{
strict: true,
decode: (items) => new Set(items),
encode: (set) => Array.from(set.values())
}
)

Please note that to define the target schema, we used Schema.typeSchema. This is because the decoding/encoding of the elements is already handled by the from schema, Schema.Array(itemSchema).

Example: Trim Whitespace

Here's how to use the transform function to trim whitespace from strings:

ts
import { Schema } from "@effect/schema"
 
const transformedSchema = Schema.transform(
// Source schema: accepts any string
Schema.String,
// Target schema: also accepts any string
Schema.String,
{
strict: true,
// Trim the string during decoding
decode: (s) => s.trim(),
// No change during encoding
encode: (s) => s
}
)
ts
import { Schema } from "@effect/schema"
 
const transformedSchema = Schema.transform(
// Source schema: accepts any string
Schema.String,
// Target schema: also accepts any string
Schema.String,
{
strict: true,
// Trim the string during decoding
decode: (s) => s.trim(),
// No change during encoding
encode: (s) => s
}
)

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:

ts
import { Schema } from "@effect/schema"
 
const transformedSchema = Schema.transform(
// Source schema: accepts any string
Schema.String,
// Target schema now only accepts strings that are trimmed
Schema.String.pipe(Schema.filter((s) => s === s.trim())),
{
strict: true,
// Trim the string during decoding
decode: (s) => s.trim(),
// No change during encoding
encode: (s) => s
}
)
ts
import { Schema } from "@effect/schema"
 
const transformedSchema = Schema.transform(
// Source schema: accepts any string
Schema.String,
// Target schema now only accepts strings that are trimmed
Schema.String.pipe(Schema.filter((s) => s === s.trim())),
{
strict: true,
// Trim the string during decoding
decode: (s) => s.trim(),
// No change during encoding
encode: (s) => s
}
)

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:

ts
import { Schema } from "@effect/schema"
import { Number } from "effect"
 
const clamp =
(minimum: number, maximum: number) =>
<A extends number, I, R>(self: Schema.Schema<A, I, R>) =>
Schema.transform(
self,
self.pipe(
Schema.typeSchema,
Schema.filter((a) => a <= minimum || a >= maximum)
),
{
Argument of type '{ strict: true; decode: (a: A) => number; encode: (a: A) => A; }' is not assignable to parameter of type '{ readonly decode: (fromA: A, fromI: I) => A; readonly encode: (toI: A, toA: A) => A; readonly strict?: true | undefined; } | { readonly decode: (fromA: A, fromI: I) => unknown; readonly encode: (toI: A, toA: A) => unknown; readonly strict: false; }'. The types returned by 'decode(...)' are incompatible between these types. Type 'number' is not assignable to type 'A'. 'number' is assignable to the constraint of type 'A', but 'A' could be instantiated with a different subtype of constraint 'number'.2345Argument of type '{ strict: true; decode: (a: A) => number; encode: (a: A) => A; }' is not assignable to parameter of type '{ readonly decode: (fromA: A, fromI: I) => A; readonly encode: (toI: A, toA: A) => A; readonly strict?: true | undefined; } | { readonly decode: (fromA: A, fromI: I) => unknown; readonly encode: (toI: A, toA: A) => unknown; readonly strict: false; }'. The types returned by 'decode(...)' are incompatible between these types. Type 'number' is not assignable to type 'A'. 'number' is assignable to the constraint of type 'A', but 'A' could be instantiated with a different subtype of constraint 'number'.
strict: true,
decode: (a) => Number.clamp(a, { minimum, maximum }),
encode: (a) => a
}
)
ts
import { Schema } from "@effect/schema"
import { Number } from "effect"
 
const clamp =
(minimum: number, maximum: number) =>
<A extends number, I, R>(self: Schema.Schema<A, I, R>) =>
Schema.transform(
self,
self.pipe(
Schema.typeSchema,
Schema.filter((a) => a <= minimum || a >= maximum)
),
{
Argument of type '{ strict: true; decode: (a: A) => number; encode: (a: A) => A; }' is not assignable to parameter of type '{ readonly decode: (fromA: A, fromI: I) => A; readonly encode: (toI: A, toA: A) => A; readonly strict?: true | undefined; } | { readonly decode: (fromA: A, fromI: I) => unknown; readonly encode: (toI: A, toA: A) => unknown; readonly strict: false; }'. The types returned by 'decode(...)' are incompatible between these types. Type 'number' is not assignable to type 'A'. 'number' is assignable to the constraint of type 'A', but 'A' could be instantiated with a different subtype of constraint 'number'.2345Argument of type '{ strict: true; decode: (a: A) => number; encode: (a: A) => A; }' is not assignable to parameter of type '{ readonly decode: (fromA: A, fromI: I) => A; readonly encode: (toI: A, toA: A) => A; readonly strict?: true | undefined; } | { readonly decode: (fromA: A, fromI: I) => unknown; readonly encode: (toI: A, toA: A) => unknown; readonly strict: false; }'. The types returned by 'decode(...)' are incompatible between these types. Type 'number' is not assignable to type 'A'. 'number' is assignable to the constraint of type 'A', but 'A' could be instantiated with a different subtype of constraint 'number'.
strict: true,
decode: (a) => Number.clamp(a, { minimum, maximum }),
encode: (a) => a
}
)

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:

  1. Using Type Assertion: Adding a type cast can enforce the return type to be treated as type A:

    ts
    decode: (a) => Number.clamp(a, { minimum, maximum }) as A
    ts
    decode: (a) => Number.clamp(a, { minimum, maximum }) as A
  2. 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:

    ts
    import { Schema } from "@effect/schema"
    import { Number } from "effect"
     
    const clamp =
    (minimum: number, maximum: number) =>
    <A extends number, I, R>(self: Schema.Schema<A, I, R>) =>
    Schema.transform(
    self,
    self.pipe(
    Schema.typeSchema,
    Schema.filter((a) => a >= minimum && a <= maximum)
    ),
    {
    strict: false,
    decode: (a) => Number.clamp(a, { minimum, maximum }),
    encode: (a) => a
    }
    )
    ts
    import { Schema } from "@effect/schema"
    import { Number } from "effect"
     
    const clamp =
    (minimum: number, maximum: number) =>
    <A extends number, I, R>(self: Schema.Schema<A, I, R>) =>
    Schema.transform(
    self,
    self.pipe(
    Schema.typeSchema,
    Schema.filter((a) => a >= minimum && a <= maximum)
    ),
    {
    strict: false,
    decode: (a) => Number.clamp(a, { minimum, maximum }),
    encode: (a) => a
    }
    )

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:

ConstructorDescription
ParseResult.succeedIndicates a successful transformation, where no errors occurred.
ParseResult.failSignals 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.

ts
import { ParseResult, Schema } from "@effect/schema"
 
export const NumberFromString = Schema.transformOrFail(
Schema.String, // Source schema: accepts any string
Schema.Number, // Target schema: expects a number
{
strict: true, // optional but you get better error messages from TypeScript
decode: (input, options, ast) => {
const parsed = parseFloat(input)
if (isNaN(parsed)) {
return ParseResult.fail(
new ParseResult.Type(
ast,
input,
"Failed to convert string to number"
)
)
}
return ParseResult.succeed(parsed)
},
encode: (input, options, ast) => ParseResult.succeed(input.toString())
}
)
ts
import { ParseResult, Schema } from "@effect/schema"
 
export const NumberFromString = Schema.transformOrFail(
Schema.String, // Source schema: accepts any string
Schema.Number, // Target schema: expects a number
{
strict: true, // optional but you get better error messages from TypeScript
decode: (input, options, ast) => {
const parsed = parseFloat(input)
if (isNaN(parsed)) {
return ParseResult.fail(
new ParseResult.Type(
ast,
input,
"Failed to convert string to number"
)
)
}
return ParseResult.succeed(parsed)
},
encode: (input, options, ast) => ParseResult.succeed(input.toString())
}
)

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:

ts
import { ParseResult, Schema, TreeFormatter } from "@effect/schema"
import { Effect } from "effect"
 
// Define an API call function
const api = (url: string): Effect.Effect<unknown, Error> =>
Effect.tryPromise({
try: () =>
fetch(url).then((res) => {
if (res.ok) {
return res.json() as Promise<unknown>
}
throw new Error(String(res.status))
}),
catch: (e) => new Error(String(e))
})
 
const PeopleId = Schema.String.pipe(Schema.brand("PeopleId"))
 
// Define a schema with async transformation
const PeopleIdFromString = Schema.transformOrFail(Schema.String, PeopleId, {
strict: true,
decode: (s, _, ast) =>
Effect.mapBoth(api(`https://swapi.dev/api/people/${s}`), {
onFailure: (e) => new ParseResult.Type(ast, s, e.message),
onSuccess: () => s
}),
encode: ParseResult.succeed
})
 
const decode = (id: string) =>
Effect.mapError(Schema.decodeUnknown(PeopleIdFromString)(id), (e) =>
TreeFormatter.formatErrorSync(e)
)
 
Effect.runPromiseExit(decode("1")).then(console.log)
/*
Output:
{ _id: 'Exit', _tag: 'Success', value: '1' }
*/
 
Effect.runPromiseExit(decode("fail")).then(console.log)
/*
Output:
{
_id: 'Exit',
_tag: 'Failure',
cause: {
_id: 'Cause',
_tag: 'Fail',
failure: '(string <-> string & Brand<"PeopleId">)\n' +
'└─ Transformation process failure\n' +
' └─ Error: 404'
}
}
*/
ts
import { ParseResult, Schema, TreeFormatter } from "@effect/schema"
import { Effect } from "effect"
 
// Define an API call function
const api = (url: string): Effect.Effect<unknown, Error> =>
Effect.tryPromise({
try: () =>
fetch(url).then((res) => {
if (res.ok) {
return res.json() as Promise<unknown>
}
throw new Error(String(res.status))
}),
catch: (e) => new Error(String(e))
})
 
const PeopleId = Schema.String.pipe(Schema.brand("PeopleId"))
 
// Define a schema with async transformation
const PeopleIdFromString = Schema.transformOrFail(Schema.String, PeopleId, {
strict: true,
decode: (s, _, ast) =>
Effect.mapBoth(api(`https://swapi.dev/api/people/${s}`), {
onFailure: (e) => new ParseResult.Type(ast, s, e.message),
onSuccess: () => s
}),
encode: ParseResult.succeed
})
 
const decode = (id: string) =>
Effect.mapError(Schema.decodeUnknown(PeopleIdFromString)(id), (e) =>
TreeFormatter.formatErrorSync(e)
)
 
Effect.runPromiseExit(decode("1")).then(console.log)
/*
Output:
{ _id: 'Exit', _tag: 'Success', value: '1' }
*/
 
Effect.runPromiseExit(decode("fail")).then(console.log)
/*
Output:
{
_id: 'Exit',
_tag: 'Failure',
cause: {
_id: 'Cause',
_tag: 'Fail',
failure: '(string <-> string & Brand<"PeopleId">)\n' +
'└─ Transformation process failure\n' +
' └─ Error: 404'
}
}
*/

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:

ts
import { ParseResult, Schema, TreeFormatter } from "@effect/schema"
import { Context, Effect, Layer } from "effect"
 
const Fetch = Context.GenericTag<"Fetch", typeof fetch>("Fetch")
 
// API call function with dependency
const api = (url: string): Effect.Effect<unknown, Error, "Fetch"> =>
Fetch.pipe(
Effect.flatMap((fetch) =>
Effect.tryPromise({
try: () =>
fetch(url).then((res) => {
if (res.ok) {
return res.json() as Promise<unknown>
}
throw new Error(String(res.status))
}),
catch: (e) => new Error(String(e))
})
)
)
 
const PeopleId = Schema.String.pipe(Schema.brand("PeopleId"))
 
const PeopleIdFromString = Schema.transformOrFail(Schema.String, PeopleId, {
strict: true,
decode: (s, _, ast) =>
Effect.mapBoth(api(`https://swapi.dev/api/people/${s}`), {
onFailure: (e) => new ParseResult.Type(ast, s, e.message),
onSuccess: () => s
}),
encode: ParseResult.succeed
})
 
const decode = (id: string) =>
Effect.mapError(Schema.decodeUnknown(PeopleIdFromString)(id), (e) =>
TreeFormatter.formatErrorSync(e)
)
 
const FetchLive = Layer.succeed(Fetch, fetch)
 
Effect.runPromiseExit(decode("1").pipe(Effect.provide(FetchLive))).then(
console.log
)
/*
Output:
{ _id: 'Exit', _tag: 'Success', value: '1' }
*/
 
Effect.runPromiseExit(decode("fail").pipe(Effect.provide(FetchLive))).then(
console.log
)
/*
Output:
{
_id: 'Exit',
_tag: 'Failure',
cause: {
_id: 'Cause',
_tag: 'Fail',
failure: '(string <-> string & Brand<"PeopleId">)\n' +
'└─ Transformation process failure\n' +
' └─ Error: 404'
}
}
*/
ts
import { ParseResult, Schema, TreeFormatter } from "@effect/schema"
import { Context, Effect, Layer } from "effect"
 
const Fetch = Context.GenericTag<"Fetch", typeof fetch>("Fetch")
 
// API call function with dependency
const api = (url: string): Effect.Effect<unknown, Error, "Fetch"> =>
Fetch.pipe(
Effect.flatMap((fetch) =>
Effect.tryPromise({
try: () =>
fetch(url).then((res) => {
if (res.ok) {
return res.json() as Promise<unknown>
}
throw new Error(String(res.status))
}),
catch: (e) => new Error(String(e))
})
)
)
 
const PeopleId = Schema.String.pipe(Schema.brand("PeopleId"))
 
const PeopleIdFromString = Schema.transformOrFail(Schema.String, PeopleId, {
strict: true,
decode: (s, _, ast) =>
Effect.mapBoth(api(`https://swapi.dev/api/people/${s}`), {
onFailure: (e) => new ParseResult.Type(ast, s, e.message),
onSuccess: () => s
}),
encode: ParseResult.succeed
})
 
const decode = (id: string) =>
Effect.mapError(Schema.decodeUnknown(PeopleIdFromString)(id), (e) =>
TreeFormatter.formatErrorSync(e)
)
 
const FetchLive = Layer.succeed(Fetch, fetch)
 
Effect.runPromiseExit(decode("1").pipe(Effect.provide(FetchLive))).then(
console.log
)
/*
Output:
{ _id: 'Exit', _tag: 'Success', value: '1' }
*/
 
Effect.runPromiseExit(decode("fail").pipe(Effect.provide(FetchLive))).then(
console.log
)
/*
Output:
{
_id: 'Exit',
_tag: 'Failure',
cause: {
_id: 'Cause',
_tag: 'Fail',
failure: '(string <-> string & Brand<"PeopleId">)\n' +
'└─ Transformation process failure\n' +
' └─ Error: 404'
}
}
*/

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

ts
import { Schema } from "@effect/schema"
import { Effect } from "effect"
 
async function validateUsername(username: string) {
return Promise.resolve(username === "gcanti")
}
 
const ValidUsername = Schema.String.pipe(
Schema.filterEffect((username) =>
Effect.promise(() =>
validateUsername(username).then((valid) => valid || "Invalid username")
)
)
).annotations({ identifier: "ValidUsername" })
 
Effect.runPromise(Schema.decodeUnknown(ValidUsername)("xxx")).then(
console.log
)
/*
ParseError: ValidUsername
└─ Transformation process failure
└─ Invalid username
*/
ts
import { Schema } from "@effect/schema"
import { Effect } from "effect"
 
async function validateUsername(username: string) {
return Promise.resolve(username === "gcanti")
}
 
const ValidUsername = Schema.String.pipe(
Schema.filterEffect((username) =>
Effect.promise(() =>
validateUsername(username).then((valid) => valid || "Invalid username")
)
)
).annotations({ identifier: "ValidUsername" })
 
Effect.runPromise(Schema.decodeUnknown(ValidUsername)("xxx")).then(
console.log
)
/*
ParseError: ValidUsername
└─ Transformation process failure
└─ Invalid username
*/

String Transformations

split

Splits a string into an array of strings.

ts
import { Schema } from "@effect/schema"
 
const schema = Schema.split(",")
 
const decode = Schema.decodeUnknownSync(schema)
 
console.log(decode("")) // [""]
console.log(decode(",")) // ["", ""]
console.log(decode("a,")) // ["a", ""]
console.log(decode("a,b")) // ["a", "b"]
ts
import { Schema } from "@effect/schema"
 
const schema = Schema.split(",")
 
const decode = Schema.decodeUnknownSync(schema)
 
console.log(decode("")) // [""]
console.log(decode(",")) // ["", ""]
console.log(decode("a,")) // ["a", ""]
console.log(decode("a,b")) // ["a", "b"]

Trim

Removes whitespaces from the beginning and end of a string.

ts
import { Schema } from "@effect/schema"
 
const decode = Schema.decodeUnknownSync(Schema.Trim)
 
console.log(decode("a")) // "a"
console.log(decode(" a")) // "a"
console.log(decode("a ")) // "a"
console.log(decode(" a ")) // "a"
ts
import { Schema } from "@effect/schema"
 
const decode = Schema.decodeUnknownSync(Schema.Trim)
 
console.log(decode("a")) // "a"
console.log(decode(" a")) // "a"
console.log(decode("a ")) // "a"
console.log(decode(" a ")) // "a"

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.

ts
import { Schema } from "@effect/schema"
 
const decode = Schema.decodeUnknownSync(Schema.Lowercase)
 
console.log(decode("A")) // "a"
console.log(decode(" AB")) // " ab"
console.log(decode("Ab ")) // "ab "
console.log(decode(" ABc ")) // " abc "
ts
import { Schema } from "@effect/schema"
 
const decode = Schema.decodeUnknownSync(Schema.Lowercase)
 
console.log(decode("A")) // "a"
console.log(decode(" AB")) // " ab"
console.log(decode("Ab ")) // "ab "
console.log(decode(" ABc ")) // " abc "

If you were looking for a combinator to check if a string is lowercased, check out the Schema.Lowercased schema or the Schema.lowercased filter.

Uppercase

Converts a string to uppercase.

ts
import { Schema } from "@effect/schema"
 
const decode = Schema.decodeUnknownSync(Schema.Uppercase)
 
console.log(decode("a")) // "A"
console.log(decode(" ab")) // " AB"
console.log(decode("aB ")) // "AB "
console.log(decode(" abC ")) // " ABC "
ts
import { Schema } from "@effect/schema"
 
const decode = Schema.decodeUnknownSync(Schema.Uppercase)
 
console.log(decode("a")) // "A"
console.log(decode(" ab")) // " AB"
console.log(decode("aB ")) // "AB "
console.log(decode(" abC ")) // " ABC "

If you were looking for a combinator to check if a string is uppercased, check out the Schema.Uppercased schema or the Schema.uppercased filter.

Capitalize

Converts a string to capitalized one.

ts
import { Schema } from "@effect/schema"
 
const decode = Schema.decodeUnknownSync(Schema.Capitalize)
 
console.log(decode("aa")) // "Aa"
console.log(decode(" ab")) // " ab"
console.log(decode("aB ")) // "AB "
console.log(decode(" abC ")) // " abC "
ts
import { Schema } from "@effect/schema"
 
const decode = Schema.decodeUnknownSync(Schema.Capitalize)
 
console.log(decode("aa")) // "Aa"
console.log(decode(" ab")) // " ab"
console.log(decode("aB ")) // "AB "
console.log(decode(" abC ")) // " abC "

If you were looking for a combinator to check if a string is capitalized, check out the Schema.Capitalized schema or the Schema.capitalized filter.

Uncapitalize

Converts a string to uncapitalized one.

ts
import { Schema } from "@effect/schema"
 
const decode = Schema.decodeUnknownSync(Schema.Uncapitalize)
 
console.log(decode("AA")) // "aA"
console.log(decode(" AB")) // " AB"
console.log(decode("Ab ")) // "ab "
console.log(decode(" AbC ")) // " AbC "
ts
import { Schema } from "@effect/schema"
 
const decode = Schema.decodeUnknownSync(Schema.Uncapitalize)
 
console.log(decode("AA")) // "aA"
console.log(decode(" AB")) // " AB"
console.log(decode("Ab ")) // "ab "
console.log(decode(" AbC ")) // " AbC "

If you were looking for a combinator to check if a string is uncapitalized, check out the Schema.Uncapitalized schema or the Schema.uncapitalized filter.

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.

ts
import { Schema } from "@effect/schema"
 
const schema = Schema.parseJson()
const decode = Schema.decodeUnknownSync(schema)
 
// Parse valid JSON strings
console.log(decode("{}")) // Output: {}
console.log(decode(`{"a":"b"}`)) // Output: { a: "b" }
 
// Attempting to decode an empty string results in an error
decode("")
/*
throws:
ParseError: (JsonString <-> unknown)
└─ Transformation process failure
└─ Unexpected end of JSON input
*/
ts
import { Schema } from "@effect/schema"
 
const schema = Schema.parseJson()
const decode = Schema.decodeUnknownSync(schema)
 
// Parse valid JSON strings
console.log(decode("{}")) // Output: {}
console.log(decode(`{"a":"b"}`)) // Output: { a: "b" }
 
// Attempting to decode an empty string results in an error
decode("")
/*
throws:
ParseError: (JsonString <-> unknown)
└─ Transformation process failure
└─ Unexpected end of JSON input
*/

Additionally, you can refine the parsing result by providing a schema to the parseJson constructor:

ts
import { Schema } from "@effect/schema"
 
// Schema<{ readonly a: number; }, string, never>
const schema = Schema.parseJson(Schema.Struct({ a: Schema.Number }))
ts
import { Schema } from "@effect/schema"
 
// Schema<{ readonly a: number; }, string, never>
const schema = Schema.parseJson(Schema.Struct({ a: Schema.Number }))

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.

ts
import { Schema } from "@effect/schema"
 
const decode = Schema.decodeUnknownSync(Schema.StringFromBase64)
 
console.log(decode("Zm9vYmFy")) // "foobar"
ts
import { Schema } from "@effect/schema"
 
const decode = Schema.decodeUnknownSync(Schema.StringFromBase64)
 
console.log(decode("Zm9vYmFy")) // "foobar"

StringFromBase64Url

Decodes a base64 (URL) encoded string into a UTF-8 string.

ts
import { Schema } from "@effect/schema"
 
const decode = Schema.decodeUnknownSync(Schema.StringFromBase64Url)
 
console.log(decode("Zm9vYmFy")) // "foobar"
ts
import { Schema } from "@effect/schema"
 
const decode = Schema.decodeUnknownSync(Schema.StringFromBase64Url)
 
console.log(decode("Zm9vYmFy")) // "foobar"

StringFromHex

Decodes a hex encoded string into a UTF-8 string.

ts
import { Schema } from "@effect/schema"
 
const decode = Schema.decodeUnknownSync(Schema.StringFromHex)
 
console.log(new TextEncoder().encode(decode("0001020304050607")))
/*
Output:
Uint8Array(8) [
0, 1, 2, 3,
4, 5, 6, 7
]
*/
ts
import { Schema } from "@effect/schema"
 
const decode = Schema.decodeUnknownSync(Schema.StringFromHex)
 
console.log(new TextEncoder().encode(decode("0001020304050607")))
/*
Output:
Uint8Array(8) [
0, 1, 2, 3,
4, 5, 6, 7
]
*/

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".

ts
import { Schema } from "@effect/schema"
 
const schema = Schema.NumberFromString
 
const decode = Schema.decodeUnknownSync(schema)
 
// success cases
console.log(decode("1")) // 1
console.log(decode("-1")) // -1
console.log(decode("1.5")) // 1.5
console.log(decode("NaN")) // NaN
console.log(decode("Infinity")) // Infinity
console.log(decode("-Infinity")) // -Infinity
 
// failure cases
decode("a")
/*
throws:
ParseError: NumberFromString
└─ Transformation process failure
└─ Expected NumberFromString, actual "a"
*/
ts
import { Schema } from "@effect/schema"
 
const schema = Schema.NumberFromString
 
const decode = Schema.decodeUnknownSync(schema)
 
// success cases
console.log(decode("1")) // 1
console.log(decode("-1")) // -1
console.log(decode("1.5")) // 1.5
console.log(decode("NaN")) // NaN
console.log(decode("Infinity")) // Infinity
console.log(decode("-Infinity")) // -Infinity
 
// failure cases
decode("a")
/*
throws:
ParseError: NumberFromString
└─ Transformation process failure
└─ Expected NumberFromString, actual "a"
*/

clamp

Clamps a number between a minimum and a maximum value.

ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Number.pipe(Schema.clamp(-1, 1)) // clamps the input to -1 <= x <= 1
 
const decode = Schema.decodeUnknownSync(schema)
 
console.log(decode(-3)) // -1
console.log(decode(0)) // 0
console.log(decode(3)) // 1
ts
import { Schema } from "@effect/schema"
 
const schema = Schema.Number.pipe(Schema.clamp(-1, 1)) // clamps the input to -1 <= x <= 1
 
const decode = Schema.decodeUnknownSync(schema)
 
console.log(decode(-3)) // -1
console.log(decode(0)) // 0
console.log(decode(3)) // 1

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".

ts
import { Schema } from "@effect/schema"
 
const schema = Schema.String.pipe(Schema.parseNumber)
 
const decode = Schema.decodeUnknownSync(schema)
 
console.log(decode("1")) // 1
console.log(decode("Infinity")) // Infinity
console.log(decode("NaN")) // NaN
console.log(decode("-"))
/*
throws
ParseError: (string <-> number)
└─ Transformation process failure
└─ Expected (string <-> number), actual "-"
*/
ts
import { Schema } from "@effect/schema"
 
const schema = Schema.String.pipe(Schema.parseNumber)
 
const decode = Schema.decodeUnknownSync(schema)
 
console.log(decode("1")) // 1
console.log(decode("Infinity")) // Infinity
console.log(decode("NaN")) // NaN
console.log(decode("-"))
/*
throws
ParseError: (string <-> number)
└─ Transformation process failure
└─ Expected (string <-> number), actual "-"
*/

Boolean Transformations

Not

Negates a boolean value.

ts
import { Schema } from "@effect/schema"
 
const decode = Schema.decodeUnknownSync(Schema.Not)
 
console.log(decode(true)) // false
console.log(decode(false)) // true
ts
import { Schema } from "@effect/schema"
 
const decode = Schema.decodeUnknownSync(Schema.Not)
 
console.log(decode(true)) // false
console.log(decode(false)) // true

Symbol transformations

Symbol

Transforms a string into a symbol by parsing the string using Symbol.for.

ts
import { Schema } from "@effect/schema"
 
const decode = Schema.decodeUnknownSync(Schema.Symbol)
 
console.log(decode("a")) // Symbol(a)
ts
import { Schema } from "@effect/schema"
 
const decode = Schema.decodeUnknownSync(Schema.Symbol)
 
console.log(decode("a")) // Symbol(a)

BigInt transformations

BigInt

Transforms a string into a BigInt by parsing the string using the BigInt constructor.

ts
import { Schema } from "@effect/schema"
 
const decode = Schema.decodeUnknownSync(Schema.BigInt)
 
// success cases
console.log(decode("1")) // 1n
console.log(decode("-1")) // -1n
 
// failure cases
decode("a")
/*
throws:
ParseError: bigint
└─ Transformation process failure
└─ Expected bigint, actual "a"
*/
decode("1.5") // throws
decode("NaN") // throws
decode("Infinity") // throws
decode("-Infinity") // throws
ts
import { Schema } from "@effect/schema"
 
const decode = Schema.decodeUnknownSync(Schema.BigInt)
 
// success cases
console.log(decode("1")) // 1n
console.log(decode("-1")) // -1n
 
// failure cases
decode("a")
/*
throws:
ParseError: bigint
└─ Transformation process failure
└─ Expected bigint, actual "a"
*/
decode("1.5") // throws
decode("NaN") // throws
decode("Infinity") // throws
decode("-Infinity") // throws

BigIntFromNumber

Transforms a number into a BigInt by parsing the number using the BigInt constructor.

ts
import { Schema } from "@effect/schema"
 
const decode = Schema.decodeUnknownSync(Schema.BigIntFromNumber)
const encode = Schema.encodeSync(Schema.BigIntFromNumber)
 
// success cases
console.log(decode(1)) // 1n
console.log(decode(-1)) // -1n
console.log(encode(1n)) // 1
console.log(encode(-1n)) // -1
 
// failure cases
decode(1.5)
/*
throws:
ParseError: BigintFromNumber
└─ Transformation process failure
└─ Expected BigintFromNumber, actual 1.5
*/
decode(NaN) // throws
decode(Infinity) // throws
decode(-Infinity) // throws
encode(BigInt(Number.MAX_SAFE_INTEGER) + 1n) // throws
encode(BigInt(Number.MIN_SAFE_INTEGER) - 1n) // throws
ts
import { Schema } from "@effect/schema"
 
const decode = Schema.decodeUnknownSync(Schema.BigIntFromNumber)
const encode = Schema.encodeSync(Schema.BigIntFromNumber)
 
// success cases
console.log(decode(1)) // 1n
console.log(decode(-1)) // -1n
console.log(encode(1n)) // 1
console.log(encode(-1n)) // -1
 
// failure cases
decode(1.5)
/*
throws:
ParseError: BigintFromNumber
└─ Transformation process failure
└─ Expected BigintFromNumber, actual 1.5
*/
decode(NaN) // throws
decode(Infinity) // throws
decode(-Infinity) // throws
encode(BigInt(Number.MAX_SAFE_INTEGER) + 1n) // throws
encode(BigInt(Number.MIN_SAFE_INTEGER) - 1n) // throws

clamp

Clamps a BigInt between a minimum and a maximum value.

ts
import { Schema } from "@effect/schema"
 
// clamps the input to -1n <= x <= 1n
const schema = Schema.BigIntFromSelf.pipe(Schema.clampBigInt(-1n, 1n))
 
const decode = Schema.decodeUnknownSync(schema)
 
console.log(decode(-3n)) // -1n
console.log(decode(0n)) // 0n
console.log(decode(3n)) // 1n
ts
import { Schema } from "@effect/schema"
 
// clamps the input to -1n <= x <= 1n
const schema = Schema.BigIntFromSelf.pipe(Schema.clampBigInt(-1n, 1n))
 
const decode = Schema.decodeUnknownSync(schema)
 
console.log(decode(-3n)) // -1n
console.log(decode(0n)) // 0n
console.log(decode(3n)) // 1n

Date transformations

Date

Transforms a string into a valid Date, ensuring that invalid dates, such as new Date("Invalid Date"), are rejected.

ts
import { Schema } from "@effect/schema"
 
const decode = Schema.decodeUnknownSync(Schema.Date)
 
console.log(decode("1970-01-01T00:00:00.000Z")) // 1970-01-01T00:00:00.000Z
 
decode("a")
/*
throws:
ParseError: Date
└─ Predicate refinement failure
└─ Expected Date, actual Invalid Date
*/
 
const validate = Schema.validateSync(Schema.Date)
 
console.log(validate(new Date(0))) // 1970-01-01T00:00:00.000Z
validate(new Date("Invalid Date"))
/*
throws:
ParseError: Date
└─ Predicate refinement failure
└─ Expected Date, actual Invalid Date
*/
ts
import { Schema } from "@effect/schema"
 
const decode = Schema.decodeUnknownSync(Schema.Date)
 
console.log(decode("1970-01-01T00:00:00.000Z")) // 1970-01-01T00:00:00.000Z
 
decode("a")
/*
throws:
ParseError: Date
└─ Predicate refinement failure
└─ Expected Date, actual Invalid Date
*/
 
const validate = Schema.validateSync(Schema.Date)
 
console.log(validate(new Date(0))) // 1970-01-01T00:00:00.000Z
validate(new Date("Invalid Date"))
/*
throws:
ParseError: Date
└─ Predicate refinement failure
└─ Expected Date, actual Invalid Date
*/

BigDecimal Transformations

BigDecimal

Transforms a string into a BigDecimal.

ts
import { Schema } from "@effect/schema"
 
const decode = Schema.decodeUnknownSync(Schema.BigDecimal)
 
console.log(decode(".124")) // { _id: 'BigDecimal', value: '124', scale: 3 }
ts
import { Schema } from "@effect/schema"
 
const decode = Schema.decodeUnknownSync(Schema.BigDecimal)
 
console.log(decode(".124")) // { _id: 'BigDecimal', value: '124', scale: 3 }

BigDecimalFromNumber

Transforms a number into a BigDecimal.

When encoding, this Schema will produce incorrect results if the BigDecimal exceeds the 64-bit range of a number.

ts
import { Schema } from "@effect/schema"
 
const decode = Schema.decodeUnknownSync(Schema.BigDecimalFromNumber)
 
console.log(decode(0.111)) // { _id: 'BigDecimal', value: '111', scale: 3 }
ts
import { Schema } from "@effect/schema"
 
const decode = Schema.decodeUnknownSync(Schema.BigDecimalFromNumber)
 
console.log(decode(0.111)) // { _id: 'BigDecimal', value: '111', scale: 3 }

clampBigDecimal

Clamps a BigDecimal between a minimum and a maximum value.

ts
import { Schema } from "@effect/schema"
import { BigDecimal } from "effect"
 
const schema = Schema.BigDecimal.pipe(
Schema.clampBigDecimal(BigDecimal.fromNumber(-1), BigDecimal.fromNumber(1))
)
 
const decode = Schema.decodeUnknownSync(schema)
 
console.log(decode("-2")) // { _id: 'BigDecimal', value: '-1', scale: 0 }
console.log(decode("0")) // { _id: 'BigDecimal', value: '0', scale: 0 }
console.log(decode("3")) // { _id: 'BigDecimal', value: '1', scale: 0 }
ts
import { Schema } from "@effect/schema"
import { BigDecimal } from "effect"
 
const schema = Schema.BigDecimal.pipe(
Schema.clampBigDecimal(BigDecimal.fromNumber(-1), BigDecimal.fromNumber(1))
)
 
const decode = Schema.decodeUnknownSync(schema)
 
console.log(decode("-2")) // { _id: 'BigDecimal', value: '-1', scale: 0 }
console.log(decode("0")) // { _id: 'BigDecimal', value: '0', scale: 0 }
console.log(decode("3")) // { _id: 'BigDecimal', value: '1', scale: 0 }