@traversable/zod
or zx
is a schema rewriter for zod
.
@traversable/zod
has a peer dependency on zod
(v4).
$ pnpm add @traversable/zod zod
Here's an example of importing the library:
import { z } from 'zod'
import { zx } from '@traversable/zod'
// see below for specific examples
zx.check
zx.check.writeable
zx.deepClone
zx.deepClone.writeable
zx.deepEqual
zx.deepEqual.writeable
zx.deepEqual.classic
zx.deepPartial
zx.deepPartial.writeable
zx.deepNullable
zx.deepNullable.writeable
zx.deepNonNullable
zx.deepNonNullable.writeable
zx.deepRequired
zx.deepRequired.writeable
zx.defaultValue
zx.fromConstant
zx.fromConstant.writeable
zx.fromJson
zx.fromJson.writeable
zx.toPaths
zx.toString
zx.toType
zx.typeof
zx.tagged
zx.makeLens
(🔬)zx.check
zx.check
converts a zod-schema into a super-performant type-guard.
z.parse
and z.safeParse
Function
constructor, including (as of May 2025) Cloudflare workers 🎉Here's a Bolt sandbox if you'd like to run the benchmarks yourself.
z.parse
and z.safeParse
clone the object they're parsing, and return an array of issues if any are encountered.
Those features are useful in certain contexts.
But in contexts where all you need is to know whether a value is valid or not, it'd be nice to have a faster alternative, that doesn't allocate.
zx.check
takes a zod schema, and returns a type guard. It's performance is an order of magnitude faster than z.parse
and z.safeParse
in
almost every case.
┌─────────────────┐
│ Average │
┌────────────────────┼─────────────────┤
│ z.parse (v4) │ 20.41x faster │
├────────────────────┼─────────────────┤
│ z.safeParse (v4) │ 21.05x faster │
└────────────────────┴─────────────────┘
import { z } from 'zod'
import { zx } from '@traversable/zod'
const Address = z.object({
street1: z.string(),
street2: z.optional(z.string()),
city: z.string(),
})
const addressCheck = zx.check(Address)
addressCheck({ street1: '221B Baker St', city: 'London' }) // => true
addressCheck({ street1: '221B Baker St' }) // => false
zx.check.writeable
zx.check
converts a zod-schema into a super-performant type-guard.
Compared to zx.check
, zx.check.writeable
returns
the check function in stringified ("writeable") form.
import { z } from 'zod'
import { zx } from '@traversable/zod'
const addressCheck = zx.check.writeable(
z.object({
street1: z.string(),
street2: z.optional(z.string()),
city: z.string(),
}),
{ typeName: 'Address' }
)
console.log(addressCheck)
// =>
// type Address = { street1: string; street2?: string; city: string; }
// function check(value: Address) {
// return (
// !!value &&
// typeof value === "object" &&
// typeof value.street1 === "string" &&
// (!Object.hasOwn(value, "street2") || typeof value?.street2 === "string") &&
// typeof value.city === "string"
// );
// }
zx.deepClone
zx.deepClone
lets users derive a specialized "deep copy" function that works with values that have been already validated.
Because the values have already been validated, clone times are significantly faster than alternatives like window.structuredClone
and Lodash.cloneDeep
.
Here's a Bolt sandbox if you'd like to run the benchmarks yourself.
┌─────────────────┐
│ (avg) │
┌──────────────────────────┼─────────────────┤
│ Lodash.cloneDeep │ 30.64x faster │
├──────────────────────────┼─────────────────┤
│ window.structuredClone │ 50.26x faster │
└──────────────────────────┴─────────────────┘
This article goes into more detail about what makes zx.deepClone
so fast.
import { assert } from 'vitest'
import { z } from 'zod'
import { zx } from '@traversable/zod'
const Address = z.object({
street1: z.string(),
street2: z.optional(z.string()),
city: z.string(),
})
const clone = zx.deepClone(Address)
const sherlock = { street1: '221 Baker St', street2: '#B', city: 'London' }
const harry = { street1: '4 Privet Dr', city: 'Little Whinging' }
const sherlockCloned = clone(sherlock)
const harryCloned = clone(harry)
// values are deeply equal:
assert.deepEqual(sherlockCloned, sherlock) // ✅
assert.deepEqual(harryCloned, harry) // ✅
// values are fresh copies:
assert.notEqual(sherlockCloned, sherlock) // ✅
assert.notEqual(harryCloned, harry) // ✅
zx.deepClone.writeable
zx.deepClone
lets users derive a specialized "deep clone" function that works with values that have been already validated.
Compared to zx.deepClone
, zx.deepClone.writeable
returns
the clone function in stringified ("writeable") form.
import { z } from 'zod'
import { zx } from '@traversable/zod'
const Address = z.object({
street1: z.string(),
street2: z.optional(z.string()),
city: z.string(),
})
const deepClone = zx.deepClone.writeable(
z.object({
street1: z.string(),
street2: z.optional(z.string()),
city: z.string(),
}),
{ typeName: 'Address' }
)
console.log(deepClone)
// =>
// type Address = { street1: string; street2?: string; city: string; }
// function deepClone(prev: Address) {
// return {
// street1: prev.street1,
// ...prev.street2 !== undefined && { street2: prev.street2 },
// city: prev.city
// }
// }
zx.deepEqual
zx.deepEqual
lets users derive a specialized "deep equal" function that works with values that have been already validated.
Because the values have already been validated, comparison times are significantly faster than alternatives like NodeJS.isDeepStrictEqual
and Lodash.isEqual
.
Here's a Bolt sandbox if you'd like to run the benchmarks yourself.
┌────────────────┬────────────────┐
│ Array (avg) │ Object (avg) │
┌────────────────────────────┼────────────────┼────────────────┤
│ NodeJS.isDeepStrictEqual │ 40.3x faster │ 56.5x faster │
├────────────────────────────┼────────────────┼────────────────┤
│ Lodash.isEqual │ 53.7x faster │ 60.1x faster │
└────────────────────────────┴────────────────┴────────────────┘
This article goes into more detail about what makes zx.deepEqual
so fast.
Function
constructor, including (as of May 2025) Cloudflare workers 🎉import { z } from 'zod'
import { zx } from '@traversable/zod'
const deepEqual = zx.deepEqual(
z.object({
street1: z.string(),
street2: z.optional(z.string()),
city: z.string(),
})
)
deepEqual(
{ street1: '221B Baker St', city: 'London' },
{ street1: '221B Baker St', city: 'London' }
) // => true
deepEqual(
{ street1: '221B Baker St', city: 'London' },
{ street1: '4 Privet Dr', city: 'Little Whinging' }
) // => false
zx.deepEqual.writeable
import { z } from 'zod'
import { zx } from '@traversable/zod'
const deepEqual = zx.deepEqual.writeable(
z.object({
street1: z.string(),
street2: z.optional(z.string()),
city: z.string(),
}),
{ typeName: 'Address' }
)
console.log(deepEqual)
// =>
// type Address = { street1: string; street2?: string; city: string; }
// function deepEqual(x: Address, y: Address) {
// if (x === y) return true;
// if (x.street1 !== y.street1) return false;
// if (x.street2 !== y.street2) return false;
// if (x.city !== y.city) return false;
// return true;
// }
zx.deepEqual.classic
import { z } from 'zod'
import { zx } from '@traversable/zod'
import * as vi from 'vitest'
const deepEqual = zx.deepEqual.classic(
z.object({
street1: z.string(),
street2: z.optional(z.string()),
city: z.string(),
})
)
deepEqual(
{ street1: '221B Baker St', city: 'London' },
{ street1: '221B Baker St', city: 'London' },
) // => true
deepEqual(
{ street1: '221B Baker St', city: 'London' },
{ street1: '4 Privet Dr', city: 'Little Whinging' },
) // => false
zx.fromConstant
Convert a blob of JSON data into a zod schema that represents its least upper bound.
import { zx } from '@traversable/zod'
let example = zx.fromConstant({ abc: 'ABC', def: [1, 2, 3] })
// ^? let example: z.ZodType<{ abc: 'ABC', def: [1, 2, 3] }>
console.log(zx.toString(example))
// => z.object({ abc: z.literal("ABC"), def: z.tuple([ z.literal(1), z.literal(2), z.literal(3) ]) })
zx.fromConstant.writeable
Convert a blob of JSON data into a stringified zod schema that represents its least upper bound.
import { zx } from '@traversable/zod'
let ex_01 = zx.fromConstant.writeable({ abc: 'ABC', def: [1, 2, 3] })
console.log(ex_01)
// => z.object({ abc: z.literal("ABC"), def: z.tuple([ z.literal(1), z.literal(2), z.literal(3) ]) })
zx.fromJson
Convert a blob of JSON data into a zod schema that represents its greatest lower bound.
import type { z } from 'zod'
import { zx } from '@traversable/zod'
let ex_01 = zx.fromJson({ abc: 'ABC', def: [] })
// ^? let ex_01: z.ZodObject<{ abc: z.ZodString, def: z.ZodArray<z.ZodUnknown> }>
console.log(zx.toString(ex_01))
// => z.object({ abc: z.string(), def: z.array(z.unknown()) })
let ex_02 = zx.fromJson({ abc: 'ABC', def: [123] })
// ^? let ex_01: z.ZodObject<{ abc: z.ZodString, def: z.ZodArray<z.ZodUnknown> }>
console.log(zx.toString(ex_02))
// => z.object({ abc: z.string(), def: z.array(z.number()) })
let ex_03 = zx.fromJson({ abc: 'ABC', def: [123, null]})
// ^? let ex_01: z.ZodObject<{ abc: z.ZodString, def: z.ZodArray<z.Union<[z.ZodNumber, z.ZodNull]>> }>
console.log(zx.toString(ex_03))
// => z.object({ abc: z.string(), def: z.array(z.union([z.number(), z.null()])) })
zx.fromJson.writeable
Convert a blob of JSON data into a stringified zod schema that represents its greatest lower bound.
import type { z } from 'zod'
import { zx } from '@traversable/zod'
let ex_01 = zx.fromJson.writeable({ abc: 'ABC', def: [] })
console.log(ex_01)
// => z.object({ abc: z.string(), def: z.array(z.unknown()) })
let ex_02 = zx.fromJson.writeable({ abc: 'ABC', def: [123] })
console.log(ex_02)
// => z.object({ abc: z.string(), def: z.array(z.number()) })
let ex_03 = zx.fromJson.writeable({ abc: 'ABC', def: [123, null]})
console.log(ex_03)
// => z.object({ abc: z.string(), def: z.array(z.union([z.number(), z.null()])) })
zx.deepPartial
Credit goes to @jaens for their work to detect circular schemas and prevent stack overflow.
import { z } from 'zod'
import { zx } from '@traversable/zod'
const MySchema = zx.deepPartial(z.object({ a: z.number(), b: z.object({ c: z.string() }) }))
type MySchema = z.infer<typeof MySchema>
// ^? type MySchema = { a?: number, b?: { c?: string } }
zx.deepPartial.writeable
import { z } from 'zod'
import { zx } from '@traversable/zod'
const MySchema = z.object({ a: z.number(), b: z.object({ c: z.string() }) })
console.log(zx.deepPartial.writeable(MySchema))
// =>
// z.object({
// a: z.number().optional(),
// b: z.object({
// c: z.string().optional(),
// d: z.array(z.boolean()).optional()
// }).optional()
// }).optional()
zx.deepRequired
import { z } from 'zod'
import { zx } from '@traversable/zod'
const MySchema = zx.deepRequired(z.object({ a: z.number().optional(), b: z.object({ c: z.string().optional() }) }))
type MySchema = z.infer<typeof MySchema>
// ^? type MySchema = { a: number, b: { c: string } }
zx.deepRequired.writeable
import { z } from 'zod'
import { zx } from '@traversable/zod'
const MySchema = z.object({
a: z.number().optional(),
b: z.optional(
z.object({
c: z.string(),
d: z.array(z.boolean()).optional()
})
)
})
console.log(zx.deepRequired.writeable(MySchema))
// =>
// z.object({
// a: z.number(),
// b: z.object({
// c: z.string(),
// d: z.array(z.boolean())
// })
// })
zx.deepNullable
import { z } from 'zod'
import { zx } from '@traversable/zod'
const MySchema = zx.deepNullable(z.object({ a: z.number(), b: z.object({ c: z.string() }) }))
type MySchema = z.infer<typeof MySchema>
// ^? type MySchema = { a: number | null, b: { c: string | null } | null }
zx.deepNullable.writeable
import { z } from 'zod'
import { zx } from '@traversable/zod'
const MySchema = z.object({
a: z.number().optional(),
b: z.object({
c: z.string(),
d: z.array(z.boolean()).optional()
})
})
console.log(zx.deepNullable.writeable(MySchema))
// =>
// z.object({
// a: z.number().nullable(),
// b: z.object({
// c: z.string().nullable(),
// d: z.array(z.boolean().nullable()).nullable()
// }).nullable()
// }).nullable()
zx.deepNonNullable
import { z } from 'zod'
import { zx } from '@traversable/zod'
const MySchema = zx.deepNonNullable(
z.object({
a: z.number().nullable(),
b: z.object({
c: z.string().nullable(),
}),
})
)
type MySchema = z.infer<typeof MySchema>
// ^? type MySchema = { a: number, b: { c: string } }
zx.deepNonNullable.writeable
import { z } from 'zod'
import { zx } from '@traversable/zod'
const MySchema = z.object({
a: z.number().nullable(),
b: z.object({
c: z.string().nullable(),
})
})
console.log(zx.deepNonNullable.writeable(MySchema))
// =>
// z.object({
// a: z.number(),
// b: z.object({
// c: z.string(),
// })
// })
zx.deepReadonly
import { z } from 'zod'
import { zx } from '@traversable/zod'
const MySchema = zx.deepReadonly(z.object({ a: z.number(), b: z.object({ c: z.string() }) }))
type MySchema = z.infer<typeof MySchema>
// ^? type MySchema = { readonly a: number, readonly b: { readonly c: string } }
zx.deepReadonly.writeable
import { z } from 'zod'
import { zx } from '@traversable/zod'
const MySchema = z.object({ a: z.number(), b: z.object({ c: z.string() }) })
console.log(zx.deepReadonly.writeable(MySchema))
// =>
// z.object({
// a: z.number().readonly(),
// b: z.object({
// c: z.string().readonly(),
// }.readonly())
// }.readonly())
zx.defaultValue
zx.defaultValues
converts a zod schema into a "default value' that respects the structure of the schema.
A common use case for zx.defaultValue
is creating default values for forms.
By default, zx.defaultValue
does not make any assumptions about what "default" means for primitive types,
which is why it returns undefined
when it encounters a leaf value. This behavior is configurable.
import { z } from 'zod'
import { zx } from '@traversable/zod'
const MySchema = z.object({
a: z.number(),
b: z.object({
c: z.string(),
d: z.array(z.boolean())
})
})
// by default, primitives are initialized as `undefined`:
const defaultOne = zx.defaultValue(MySchema)
console.log(defaultOne) // => { a: undefined, b: { c: undefined, d: [] } }
// to configure this behavior, use the `fallbacks` property:
const defaultTwo = zx.defaultValue(MySchema, { fallbacks: { number: 0, string: '' } })
console.log(defaultTwo) // => { a: 0, b: { c: '', d: [] } }
zx.toPaths
zx.toPaths
converts a zod schema into an array of "paths" that represent the schema.
import { z } from 'zod'
import { zx } from '@traversable/zod'
console.log(
zx.toPaths(z.object({ a: z.object({ c: z.string() }), b: z.number() }))
) // => [["a", "c"], ["b"]]
zx.toString
Convert a zod schema into a string that constructs the same zod schema.
Useful for writing/debugging tests that involve randomly generated schemas.
import { z } from 'zod'
import { zx } from '@traversable/zod'
console.log(
zx.toString(
z.templateLiteral([1n])
)
) // => z.templateLiteral([1n])
console.log(
zx.toString(
z.map(z.array(z.boolean()), z.set(z.number().optional()))
)
) // => z.map(z.array(z.boolean()), z.set(z.number().optional()))
console.log(
zx.toString(
z.tuple([
z.number().min(0).lt(2),
z.number().multipleOf(2).nullable(),
])
)
) // => z.tuple([z.number().min(0).lt(2), z.number().multipleOf(2).nullable()])
zx.toType
Convert a zod schema into a string that represents its type.
To preserve JSDoc annotations for object properties, pass preserveJsDocs: true
in the options object.
If the property's metadata includes an example
property, the example will be escaped and included
as an @escape
tag.
By default, the type will be returned as an "inline" type.
To give the type a name, use the typeName
option.
import { z } from 'zod'
import { zx } from '@traversable/zod'
console.log(
zx.toType(
z.object({
a: z.optional(z.literal(1)),
b: z.literal(2),
c: z.optional(z.literal(3))
})
)
) // => { a?: 1, b: 2, c?: 3 }
console.log(
zx.toType(
z.intersection(
z.object({ a: z.literal(1) }),
z.object({ b: z.literal(2) })
)
)
) // => { a: 1 } & { b: 2 }
console.log(
zx.toType(
z.templateLiteral([
z.literal(['a', 'b']),
' ',
z.literal(['c', 'd']),
' ',
z.literal(['e', 'f'])
])
)
) // => "a c e" | "a c f" | "a d e" | "a d f" | "b c e" | "b c f" | "b d e" | "b d f"
// To give the generated type a name, use the `typeName` option:
console.log(
zx.toType(
z.object({ a: z.optional(z.number()) }),
{ typeName: 'MyType' }
)
) // => type MyType = { a?: number }
// To preserve JSDoc annotations, use the `preserveJsDocs` option:
console.log(
zx.toType(
z.object({
street1: z.string().meta({ describe: 'Street 1 name' }),
street2: z.string().optional().meta({ describe: 'Street 2 name', example: 'Unit B' }),
city: z.string(),
}),
{ typeName: 'Address', preserveJsDocs: true }
)
)
// =>
// type Address = {
// /**
// * Street 1 name
// */
// street1: string
// /**
// * Street 2 name
// * @example "Unit B"
// */
// street2?: string
// city: string
// }
zx.typeof
zx.typeof
returns the "type" (or tag) of a zod schema.
import { z } from 'zod'
import { zx } from '@traversable/zod'
console.log(zx.typeof(z.string())) // => "string"
zx.tagged
zx.tagged
lets you construct a type-guard that identifies the type of zod schema you have.
import { z } from 'zod'
import { zx } from '@traversable/zod'
zx.tagged('object', z.object({})) // true
zx.tagged('array', z.string()) // false
zx.makeLens
zx.makeLens
still experimental (🔬). Use in production with care.
zx.makeLens
accepts a zod schema (classic, v4) as its first argument, and a
"selector function" as its second argument.
An optic is a generalization of a lens, but since most people use "lens" to refer to optics generally, they are sometimes used interchangeably in this document.
With zx.makeLens
, you use a selector function to build up an optic via a series of property accesses.
Let's look at a few examples to make things more concrete.
For our first example, let's create a lens that focuses on a structure's "a[0]"
path:
import { z } from 'zod'
import { zx } from '@traversable/zod'
//////////////////////////
/// example #1: Lens ///
//////////////////////////
const Schema = z.object({ a: z.tuple([z.string(), z.bigint()]) })
// Use autocompletion to "select" what you want to focus:
// ↆↆↆↆↆↆ
const Lens = zx.makeLens(Schema, $ => $.a[0])
Lens
// ^? const Lens: zx.Lens<{ a: [string, bigint] }, string>
// 𐙘___________________𐙘 𐙘____𐙘
// structure focus
// Lenses have 3 properties:
///////////////
// #1:
// Lens.get -- Given a structure,
// returns the focus
const ex_01 = Lens.get({ a: ['hi', 0n] })
// 𐙘_____________𐙘
// structure
console.log(ex_01) // => "hi"
// 𐙘𐙘
// focus
///////////////
// #2:
// Lens.set -- Given a new focus and a structure,
// sets the new focus & returns the structure
const ex_02 = Lens.set(`hey, ho, let's go`, { a: ['', 0n] })
// 𐙘_______________𐙘 𐙘___________𐙘
// new focus structure
console.log(ex_02) // => { a: ["hey, ho, let's go", 0n] }
// 𐙘_______________𐙘
// new focus
/////////////////
// #3:
// Lens.modify -- Given a "modify" callback and a structure,
// applies the callback to the focus & returns the structure
const ex_03 = Lens.modify((str) => str.toUpperCase(), { a: [`hey, ho`, 0n] })
// 𐙘_______________________𐙘 𐙘__________________𐙘
// callback structure
console.log(ex_03) // => { a: ["HEY, HO", 0n] }
// 𐙘_____𐙘
// new focus
// Note that if your callback changes the focus type,
// that will be reflected in the return type as well:
const ex_04 = Lens.modify((str) => str.length > 0, { a: ['', 0n] })
// 𐙘____________________𐙘 𐙘___________𐙘
// callback structure
console.log(ex_04) // => { a: [false, 0n] }
// ^? const ex_04: { a: [boolean, bigint] }
// 𐙘_____𐙘
// new focus
When you use zx.makeLens
on a union type, you get back a different kind
of lens called a prism.
Let's see how prisms differ from lenses:
import { z } from 'zod'
import { zx } from '@traversable/zod'
///////////////////////////
/// example #2: Prism ///
///////////////////////////
const Schema = z.union([
z.object({ tag: z.literal('ONE'), ghi: z.number() }),
z.object({ tag: z.literal('TWO') })
])
// Let's focus on the first union member's "ghi" property.
// If a discriminant can be inferred, autocompletion allows
// you to select that member by its discriminant,
// prefixed by `ꖛ`:
//
// ↆↆↆↆↆ
const Prism = zx.makeLens(Schema, $ => $.ꖛONE.ghi)
Prism
// ^? Prism: zx.Prism<{ tag: "ONE", ghi: number } | { tag: "TWO" }, number | undefined>
// 𐙘________________________________________𐙘 𐙘________________𐙘
// structure focus
// Prisms have the same 3 properties as lenses,
// but they behave like **pattern matchers**
// instead of _property accessors_
///////////////
// #1:
// Prism.get -- Given a matching structure,
// returns the focus
const ex_01 = Prism.get({ tag: 'ONE', ghi: 123 })
// 𐙘____________________𐙘
// structure
console.log(ex_01) // => 123
// 𐙘𐙘𐙘
// focus
// Prism.get -- If the match fails,
// returns undefined
const ex_02 = Prism.get({ tag: 'TWO' })
// 𐙘___________𐙘
// structure
console.log(ex_02) // => undefined
// 𐙘𐙘𐙘
// no match
///////////////
// #2:
// Prism.set -- Given a new focus and a matching structure,
// sets the new focus & returns the structure
const ex_03 = Prism.set(9_000, { tag: 'ONE', ghi: 123 })
// 𐙘___𐙘 𐙘____________________𐙘
// new focus structure
console.log(ex_03) // => { tag: 'ONE', ghi: 9000 }
// 𐙘__𐙘
// new focus
// Prism.set -- If the match fails,
// returns the structure unchanged
const ex_04 = Prism.set(9000, { tag: 'TWO' })
console.log(ex_04) // => { tag: 'TWO' }
// 𐙘__________𐙘
// no match
//////////////////
// #3:
// Prism.modify -- Given a "modify" callback and a matching structure,
// applies the callback to the focus & returns the structure
// Just like with lenses, if your callback changes the focus type,
// that will be reflected in the return type:
const ex_05 = Prism.modify((n) => [n, n], { tag: 'ONE', ghi: 123 })
// 𐙘___________𐙘 𐙘____________________𐙘
// callback structure
console.log(ex_05) // => { tag: 'ONE', ghi: [123, 123] }
// ^? const ex_05: { tag: "ONE", ghi: number[] } | { tag: "TWO" }
// Prism.modify -- If the match fails,
// returns the structure unchanged
const ex_06 = Prism.modify((n) => n + 1, { tag: 'TWO' })
// 𐙘__________𐙘 𐙘___________𐙘
// callback structure
console.log(ex_06) // => { tag: 'TWO' }
// ^? const ex_06: { tag: "ONE", ghi: number } | { tag: "TWO" }
When you use zx.makeLens
on a collection type (such as z.array
or z.record
),
you get back a different kind of lens called a traversal.
Let's see how traversals differ from lenses and prisms:
import { z } from 'zod'
import { zx } from '@traversable/zod'
///////////////////////////////
/// example #3: Traversal ///
///////////////////////////////
const Schema = z.object({
a: z.array(
z.object({
b: z.number(),
c: z.string()
})
)
})
// Let's focus on the `"b"` property of each of the elements of the structure's `"a"` property:
// To indicate that you want to traverse the array,
// autocomplete the `ᣔꓸꓸ` field:
// ↆↆ
const Traversal = zx.makeLens(Schema, $ => $ => $.a.ᣔꓸꓸ.b)
Traversal
// ^? Traversal: zx.Traversal<{ a: { b: number, c: string }[] }, number>
// 𐙘_____________________________𐙘 𐙘____𐙘
// structure focus
// Traversals have the same 3 properties as lenses and prisms,
// but they behave like **for-of loops**
// instead of _property accessors_ or _patterns matchers_
///////////////
// #1:
// Traversal.get -- Given a matching structure,
// returns all of the focuses
const ex_01 = Traversal.get({ a: [{ b: 0, c: '' }, { b: 1, c: '' }] })
// 𐙘_____________________________________𐙘
// structure
console.log(ex_01) // => [0, 1]
// 𐙘__𐙘
// focus
///////////////
// #2:
// Traversal.set -- Given a new focus and a matching structure, sets all of the elements
// of the collection to the new focus & returns the structure
const ex_02 = Traversal.set(9_000, { a: [{ b: 0, c: '' }, { b: 1, c: '' }] })
// 𐙘___𐙘 𐙘_____________________________________𐙘
// new focus structure
console.log(ex_02) // => { a: [{ b: 9000, c: '' }, { b: 9000, c: '' }] }
// 𐙘__𐙘 𐙘__𐙘
// new focus new focus
//////////////////
// #3:
// Traversal.modify -- Given a "modify" callback and a matching structure,
// applies the callback to _each_ focus & returns the structure
// Just like with lenses & prisms, if your callback changes the focus type,
// that will be reflected in the return type:
const ex_03 = Traversal.modify((n) => [n, n + 1], { a: [{ b: 0, c: '' }, { b: 1, c: '' }] })
// 𐙘______________𐙘 𐙘_____________________________________𐙘
// callback structure
console.log(ex_03) // => { a: [{ b: [0, 1], c: '' }, { b: [1, 2], c: '' }] }
// ^? const ex_03: { a: { b: number[], c: string }[] }
// 𐙘______𐙘
// new focus
zx.fold
zx.fold
is an advanced API.
Use zx.fold
to define a recursive traversal of a zod schema. Useful when building a schema rewriter.
zx.fold
is a powertool. Most of @traversable/zod
uses zx.fold
under the hood.
Compared to the rest of the library, it's fairly "low-level", so unless you're doing something pretty advanced you probably won't need to use it directly.
Let's write a function that takes an arbitrary zod schema as input and stringifies it.
This functionality is already available off-the shelf via zx.toString
.
We'll be building this example from scratch using zx.fold
for illustrative purposes.
import { zx } from '@traversable/zod'
const toString = zx.fold<string>((x) => {
// 𐙘____𐙘 this type parameter fills in the "holes" below
switch (true) {
case zx.tagged('null')(x): return 'z.null()'
case zx.tagged('number')(x): return 'z.number()'
case zx.tagged('string')(x): return 'z.string()'
case zx.tagged('boolean')(x): return 'z.boolean()'
case zx.tagged('undefined')(x): return 'z.undefined()'
case zx.tagged('array')(x): return `${x._zod.def.element}.array()`
// ^? method element: string
case zx.tagged('optional')(x): return `${x._zod.def.innerType}.optional()`
// ^? method innerType: string
case zx.tagged('tuple')(x): return `z.tuple([${x._zod.def.items.join(', ')}])`
// ^? method items: string[]
case zx.tagged('record')(x): return `z.record(${x._zod.def.keyType}, ${x._zod.def.valueType})`
// ^? method keyType: string
case zx.tagged('object')(x):
return `z.object({ ${Object.entries(x._zod.def.shape).map(([k, v]) => `${k}: ${v}`).join(', ')} })`
// ^? method shape: { [x: string]: string }
default: throw Error(`Unimplemented: ${x._zod.def.type}`)
// ^^ there's nothing stopping you from implementing the rest!
}
})
// Let's test it out:
console.log(
zx.toString(
z.object({ A: z.array(z.string()), B: z.optional(z.tuple([z.number(), z.boolean()])) })
)
)
// => z.object({ A: z.array(z.string()), B: z.optional(z.tuple([z.number(), z.boolean()])) })
Our "naive" implementation is actually more robust than it might seem -- in fact, that's how zx.toString
is actually defined.
zx.Functor
zx.Functor
is an advanced API
zx.Functor
is the primary abstraction that powers @traversable/zod
.
zx.Functor
is a powertool. Most of @traversable/zod
uses zx.Functor
under the hood.
Compared to the rest of the library, it's fairly "low-level", so unless you're doing something pretty advanced you probably won't need to use it directly.