@traversable/valibot
v0.0.26
Published
<br> <h1 align="center">แฏ๐๐ฟ๐ฎ๐๐ฒ๐ฟ๐๐ฎ๐ฏ๐น๐ฒ/๐๐ฎ๐น๐ถ๐ฏ๐ผ๐</h1> <br>
Readme
Requirements
@traversable/valibot has a peer dependency on valibot.
What's it all about?
Read the blog post, Introducing: @traversable/valibot (3 min read).
Getting started
$ pnpm add @traversable/valibot valibotHere's an example of importing the library:
import * as v from 'valibot'
import { vx } from '@traversable/valibot'
// see below for specific examplesTable of contents
Fuzz-tested, production ready
vx.checkvx.check.writeablevx.deepClonevx.deepClone.writeablevx.deepEqualvx.deepEqual.writeablevx.defaultValuevx.fromConstantvx.fromConstant.writeablevx.fromJsonvx.fromJson.writeablevx.toStringvx.toType
Advanced
Features
vx.check
vx.check converts a Valibot schema into a super-performant type-guard.
Notes
- Better performance than
v.is,v.parseandv.safeParse - Works in any environment that supports defining functions using the
Functionconstructor, 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
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' }) // => falseSee 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
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.deepClone
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
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
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.deepEqual
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
Functionconstructor, including (as of May 2025) Cloudflare workers ๐
Example
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' }
) // => falseSee 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
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.defaultValue
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.defaultValuedoes not make any assumptions about what "default" means for primitive types, which is why it returnsundefinedwhen it encounters a leaf value. This behavior is configurable.
Example
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: [] } }vx.fromConstant
Convert a blob of JSON data into a valibot schema that represents the blob's least upper bound.
Example
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.fromConstant.writeable
Convert a blob of JSON data into a stringified valibot schema that represents the blob's least upper bound.
Example
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
Convert a blob of JSON data into a valibot schema that represents the blob's greatest lower bound.
Example
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.fromJson.writeable
Convert a blob of JSON data into a stringified valibot schema that represents the blob's greatest lower bound.
Example
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.toString
Convert a valibot schema into a string that constructs the same valibot schema.
Useful for writing/debugging tests that involve randomly generated schemas.
Example
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())vx.toType
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
typeNameoption.
Example
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
// }Advanced Features
vx.fold
[!NOTE]
vx.foldis 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
import * as v from 'valibot'
import { faker } from '@faker-js/faker'
import { fold, tagged } from '@traversable/valibot'
type Fake = () => unknown
const fake = fold<Fake>((x) => {
// ๐__๐ 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]
// }vx.Functor
[!NOTE]
vx.Functoris 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.
