<br> <h1 align="center">įÆššæš®šš²šæšš®šÆš¹š²/šš®š¹š¶šÆš¼š</h1> <br>
npm install @traversable/valibot@traversable/valibot or vx is a schema rewriter for Valibot.
@traversable/valibot has a peer dependency on valibot.
Read the blog post, Introducing: @traversable/valibot (3 min read).
``bash`
$ pnpm add @traversable/valibot valibot
Here's an example of importing the library:
`typescript
import * as v from 'valibot'
import { vx } from '@traversable/valibot'
// see below for specific examples
`
- vx.check
- vx.check.writeable
- vx.deepClone
- vx.deepClone.writeable
- vx.deepEqual
- vx.deepEqual.writeable
- vx.defaultValue
- vx.fromConstant
- vx.fromConstant.writeable
- vx.fromJson
- vx.fromJson.writeable
- vx.toString
- vx.toType
vx.check converts a Valibot schema into a super-performant type-guard.
#### Notes
- Better performance than v.is, v.parse and v.safeParseFunction
- Works in any environment that supports defining functions using the constructor, including (as of May 2025) Cloudflare workers š
#### Performance comparison
Here's a Bolt sandbox if you'd like to run the benchmarks yourself.
``
āāāāāāāāāāāāāāāāāāā
ā Average ā
āāāāāāāāāāāāāāāāā¼āāāāāāāāāāāāāāāāāā¤
ā v.is ā 40.22x faster ā
āāāāāāāāāāāāāāāāā¼āāāāāāāāāāāāāāāāāā¤
ā v.parse ā 52.34x faster ā
āāāāāāāāāāāāāāāāā¼āāāāāāāāāāāāāāāāāā¤
ā v.safeParse ā 54.18x faster ā
āāāāāāāāāāāāāāāāā“āāāāāāāāāāāāāāāāāā
v.parse and v.safeParse clone the object they're parsing, and return an array of issues if any are encountered.
Those features are incredibly useful in the right context.
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.
vx.check takes a valibot schema, and returns a type guard. It's performance is more than an order of magnitude faster than v.parse and v.safeParse.
#### Example
`typescript
import * as v from 'valibot'
import { vx } from '@traversable/valibot'
const Address = v.object({
street1: v.string(),
street2: v.exactOptional(v.string()),
city: v.string(),
})
const addressCheck = vx.check(Address)
addressCheck({ street1: '221B Baker St', city: 'London' }) // => true
addressCheck({ street1: '221B Baker St' }) // => false
`
#### See also
- vx.check.writeable
vx.check.writable converts a Valibot schema into a super-performant type-guard.
Compared to vx.check, vx.check.writeable returns
the check function in _stringified_ ("writeable") form.
#### Notes
- Useful when you're consuming a set of valibot schemas and writing them all to disc
- Also useful for testing purposes or for troubleshooting, since it gives you a way to "see" exactly what the check functions check
#### Example
`typescript
import * as v from 'valibot'
import { vx } from '@traversable/valibot'
const addressCheck = vx.check.writeable(
v.object({
street1: v.string(),
street2: v.exactOptional(v.string()),
city: v.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"
// );
// }
`
#### See also
- vx.check
vx.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.
#### Performance comparison
Here's a Bolt sandbox if you'd like to run the benchmarks yourself.
``
āāāāāāāāāāāāāāāāāāā
ā Average ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāā¼āāāāāāāāāāāāāāāāāā¤
ā Lodash.cloneDeep ā 9.18x faster ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāā¼āāāāāāāāāāāāāāāāāā¤
ā window.structuredClone ā 19.41x faster ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāā“āāāāāāāāāāāāāāāāāā
This article goes into more detail about what makes vx.deepClone so fast.
#### Example
`typescript
import { assert } from 'vitest'
import * as v from 'valibot'
import { vx } from '@traversable/valibot'
const Address = v.object({
street1: v.string(),
street2: v.exactOptional(v.string()),
city: v.string(),
})
const clone = vx.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) // ā
`
#### See also
- vx.deepClone.writeable
vx.deepClone lets users derive a specialized "deep clone" function that works with values that have been already validated.
Compared to vx.deepClone, vx.deepClone.writeable returns
the clone function in _stringified_ ("writeable") form.
#### Example
`typescript
import * as v from 'valibot'
import { vx } from '@traversable/valibot'
const deepClone = vx.deepClone.writeable(
v.object({
street1: v.string(),
street2: v.exactOptional(v.string()),
city: v.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
// }
// }
`
#### See also
- vx.deepClone
vx.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.
#### Performance comparison
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 vx.deepEqual so fast.
#### Notes
- Works in any environment that supports defining functions using the Function constructor, including (as of May 2025) Cloudflare workers š
#### Example
`typescript
import * as v from 'valibot'
import { vx } from '@traversable/valibot'
const deepEqual = vx.deepEqual(
v.object({
street1: v.string(),
street2: v.exactOptional(v.string()),
city: v.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
`
#### See also
- vx.deepEqual.writeable
#### Notes
- Useful when you're consuming a set of valibot schemas and writing them all to disc
- Also useful for testing purposes or for troubleshooting, since it gives you a way to "see" exactly what the deep equal functions are doing
#### Example
`typescript
import * as v from 'valibot'
import { vx } from '@traversable/valibot'
const deepEqual = vx.deepEqual.writeable(
v.object({
street1: v.string(),
street2: v.exactOptional(v.string()),
city: v.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;
// }
`
#### See also
- vx.deepEqual
vx.defaultValues converts a Valibot schema into a "default value' that respects the structure of the schema.
A common use case for vx.defaultValue is creating default values for forms.
> [!NOTE]
> By default, vx.defaultValue does not make any assumptions about what "default" means for primitive types,undefined
> which is why it returns when it encounters a leaf value. This behavior is configurable.
#### Example
`typescript
import * as v from 'valibot'
import { vx } from '@traversable/valibot'
const MySchema = v.object({
a: v.number(),
b: v.object({
c: v.string(),
d: v.array(v.boolean())
})
})
// by default, primitives are initialized as undefined:
const defaultOne = vx.defaultValue(MySchema)
console.log(defaultOne) // => { a: undefined, b: { c: undefined, d: [] } }
// to configure this behavior, use the fallbacks property:`
const defaultTwo = vx.defaultValue(MySchema, { fallbacks: { number: 0, string: '' } })
console.log(defaultTwo) // => { a: 0, b: { c: '', d: [] } }
Convert a blob of JSON data into a valibot schema that represents the blob's least upper bound.
#### Example
`typescript
import type * as v from 'valibot'
import { vx } from '@traversable/valibot'
let example = vx.fromConstant({ abc: 'ABC', def: [1, 2, 3] })
// ^? let example: v.ObjectSchema<{ readonly abc: 'ABC', readonly def: readonly [1, 2, 3] }>
console.log(vx.toString(example))
// => v.object({ abc: v.literal("ABC"), def: v.tuple([v.literal(1), v.literal(2), v.literal(3)]) })
`
#### See also
- vx.fromJson
- vx.fromConstant.writeable
Convert a blob of JSON data into a _stringified_ valibot schema that represents the blob's least upper bound.
#### Example
`typescript
import { vx } from '@traversable/valibot'
let ex_01 = vx.fromConstant.writeable({ abc: 'ABC', def: [1, 2, 3] })
console.log(ex_01)
// => v.object({ abc: v.literal("ABC"), def: v.tuple([ v.literal(1), v.literal(2), v.literal(3) ]) })
`
#### See also
- vx.fromJson
- vx.fromConstant
Convert a blob of JSON data into a valibot schema that represents the blob's greatest lower bound.
#### Example
`typescript
import type * as v from '@traversable/valibot'
import { vx } from '@traversable/valibot'
let ex_01 = vx.fromJson({ abc: 'ABC', def: [] })
console.log(vx.toString(ex_01))
// => v.object({ abc: v.string(), def: v.array(v.unknown()) })
let ex_02 = vx.fromJson({ abc: 'ABC', def: [123] })
console.log(vx.toString(ex_02))
// => v.object({ abc: v.string(), def: v.array(v.number()) })
let ex_03 = vx.fromJson({ abc: 'ABC', def: [123, null]})
console.log(vx.toString(ex_03))
// => v.object({ abc: v.string(), def: v.array(v.union([v.number(), v.null()])) })
`
#### See also
- vx.fromConstant
- vx.fromJson.writeable
Convert a blob of JSON data into a _stringified_ valibot schema that represents the blob's greatest lower bound.
#### Example
`typescript
import { vx } from '@traversable/valibot'
let ex_01 = vx.fromJson.writeable({ abc: 'ABC', def: [] })
console.log(ex_01)
// => v.object({ abc: v.string(), def: v.array(v.unknown()) })
let ex_02 = vx.fromJson.writeable({ abc: 'ABC', def: [123] })
console.log(ex_02)
// => v.object({ abc: v.string(), def: v.array(v.number()) })
let ex_03 = vx.fromJson.writeable({ abc: 'ABC', def: [123, null]})
console.log(ex_03)
// => v.object({ abc: v.string(), def: v.array(v.union([v.number(), v.null()])) })
`
#### See also
- vx.fromConstant
- vx.fromJson
Convert a valibot schema into a string that constructs the same valibot schema.
Useful for writing/debugging tests that involve randomly generated schemas.
#### Example
`typescript
import * as v from 'valibot'
import { vx } from '@traversable/valibot'
console.log(
vx.toString(
v.map(v.array(v.boolean()), v.set(v.optional(v.number())))
)
) // => v.map(v.array(v.boolean()), v.set(v.optional(v.number())))
console.log(
vx.toString(
v.tupleWithRest([v.number(), v.number()], v.boolean())
)
) // => v.tupleWithRest([v.number(), v.number()], v.boolean())
`
Convert a valibot schema into a string that represents its type.
To preserve JSDoc annotations for object properties, pass preserveJsDocs: true in the options object.
> [!NOTE]
> By default, the type will be returned as an "inline" type.
> To give the type a name, use the typeName option.
#### Example
`typescript
import * as v from 'valibot'
import { vx } from '@traversable/valibot'
console.log(
vx.toType(
v.object({
a: v.exactOptional(v.literal(1)),
b: v.literal(2),
c: v.exactOptional(v.literal(3))
})
)
) // => { a?: 1, b: 2, c?: 3 }
console.log(
vx.toType(
v.intersection([
v.object({ a: v.literal(1) }),
v.object({ b: v.literal(2) })
])
)
) // => { a: 1 } & { b: 2 }
// To give the generated type a name, use the typeName option:
console.log(
vx.toType(
v.object({ a: v.exactOptional(v.number()) }),
{ typeName: 'MyType' }
)
) // => type MyType = { a?: number }
// To preserve JSDoc annotations, use the preserveJsDocs option:`
console.log(
vx.toType(
v.object({
street1: v.string().describe('Street 1 description'),
street2: v.string().exactOptional().describe('Street 2 description'),
city: v.string(),
}),
{ typeName: 'Address', preserveJsDocs: true }
)
)
// =>
// type Address = {
// /**
// * Street 1 description
// */
// street1: string
// /**
// * Street 2 description
// */
// street2?: string
// city: string
// }
> [!NOTE]
> vx.fold is an advanced API.
Use vx.fold to define a recursive traversal of a valibot schema. Useful when building a schema rewriter.
vx.fold is a powertool. Most of @traversable/valibot uses vx.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.
#### Example
Let's write a function that takes an arbitrary valibot schema, and generates mock data that satisfies the schema (a.k.a. a "faker").
> [!NOTE]
> You can play with this example on StackBlitz
`typescript
import * as v from 'valibot'
import { faker } from '@faker-js/faker'
import { fold, tagged } from '@traversable/valibot'
type Fake = () => unknown
const fake = fold
// š__š this type parameter fills in the "holes" below
switch (true) {
case tagged('array')(x): return () => faker.helpers.multiple(
() => x.item()
// ^? method items: Fake
// š__š
)
case tagged('never')(x): return () => void 0
case tagged('unknown')(x): return () => void 0
case tagged('any')(x): return () => void 0
case tagged('void')(x): return () => void 0
case tagged('null')(x): return () => null
case tagged('undefined')(x): return () => undefined
case tagged('symbol')(x): return () => Symbol()
case tagged('boolean')(x): return () => faker.datatype.boolean()
case tagged('NaN')(x): return () => NaN
case tagged('bigint')(x): return () => faker.number.bigInt()
case tagged('number')(x): return () => faker.number.float()
case tagged('string')(x): return () => faker.string.alpha()
case tagged('date')(x): return () => faker.date.recent()
case tagged('literal')(x): return () => x.literal
case tagged('enum')(x): return () => faker.helpers.arrayElement(Object.values(x.enum))
case tagged('lazy')(x): return x.getter()
case tagged('nonOptional')(x): return () => x.wrapped()
case tagged('nonNullable')(x): return () => x.wrapped()
case tagged('nonNullish')(x): return () => x.wrapped()
case tagged('nullable')(x): return () => faker.helpers.arrayElement([x.wrapped(), null])
case tagged('optional')(x): return () => faker.helpers.arrayElement([x.wrapped(), undefined])
case tagged('exactOptional')(x): return () => faker.helpers.arrayElement([x.wrapped(), undefined])
case tagged('undefinedable')(x): return () => faker.helpers.arrayElement([x.wrapped(), undefined])
case tagged('nullish')(x): return () => faker.helpers.arrayElement([x.wrapped(), null, undefined])
case tagged('set')(x): return () => new Set([x.value()])
case tagged('map')(x): return () => new Map([[x.key(), x.value()]])
case tagged('record')(x): return () => Object.fromEntries([[x.key(), x.value() ]])
case tagged('blob')(x): return () => new Blob(faker.lorem.lines().split('\n'))
case tagged('file')(x): return () => new File(faker.lorem.lines().split('\n'), faker.system.commonFileName())
case tagged('intersect')(x): return () => Object.assign({}, ...x.options.map((option) => option()))
case tagged('union')(x): return () => faker.helpers.arrayElement(x.options.map((option) => option()))
case tagged('variant')(x): return () => faker.helpers.arrayElement(x.options)
case tagged('looseTuple')(x):
case tagged('strictTuple')(x):
case tagged('tupleWithRest')(x):
case tagged('tuple')(x): return () => x.items.map((item) => item())
case tagged('looseObject')(x):
case tagged('strictObject')(x):
case tagged('objectWithRest')(x):
case tagged('object')(x): return () => Object.fromEntries(Object.entries(x.entries).map(([k, v]) => [k, v()]))
case tagged('custom')(x):
case tagged('promise')(x):
case tagged('function')(x):
case tagged('instance')(x):
case tagged('picklist')(x): { throw Error('Unsupported schema: ' + x.type) }
default: { x satisfies never; throw Error('Illegal state') }
// š_______________š
// exhaustiveness check works
}
})
// Let's test it out:
const mock = fake(
v.object({
abc: v.array(v.string()),
def: v.optional(
v.tuple([
v.number(),
v.boolean()
])
)
})
)
console.log(mock())
// => {
// abc: [
// 'annus iure consequatur',
// 'aer suus autem',
// 'delectus patrocinor deporto',
// 'benevolentia tonsor odit',
// 'stabilis dolor tres',
// 'mollitia quibusdam vociferor'
// ],
// def: [-882, false]
// }
`
> [!NOTE]
> vx.Functor is an advanced API
vx.Functor is the primary abstraction that powers @traversable/valibot.
vx.Functor is a powertool. Most of @traversable/valibot uses vx.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.