@traversable/schema
    Preparing search index...

    Module @traversable/schema - v0.0.51


    แฏ“๐˜๐—ฟ๐—ฎ๐˜ƒ๐—ฒ๐—ฟ๐˜€๐—ฎ๐—ฏ๐—น๐—ฒ/๐˜€๐—ฐ๐—ต๐—ฒ๐—บ๐—ฎ


    TypeScript schema rewriter

    NPM Version   TypeScript   License   npm  
    Static Badge   Static Badge   Static Badge  
    Demo (StackBlitz)   โ€ข   TypeScript Playground   โ€ข   npm


    A schema is a syntax tree. ASTs lend themselves to (re)-interpretation. If you're not treating your TypeScript schemas like ASTs, you're missing out.

    @traversable/schema makes it easy to do anything with a TypeScript schema.

    The idea of term rewriting comes from the programming language community. Languages like Racket and Lean invert control and give users a first-class API for rewriting and extending the language.

    Unfortunately, we don't have that kind of power in TypeScript because we're limited by the target language (JavaScript). And frankly, given how flexible JavaScript already is, exposing that kind of API would be a recipe for disaster.

    We do however have schemas, and schemas are basically ASTs.

    Let's look at a concrete example of how @traversable/schema can be used as a rewriting tool.

    For this example, we'll be using @traversable/zod, since zod is the library most users are familiar with.

    Let's write a function that takes an arbitrary zod schema as input and stringifies it.

    Note

    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/schema'

    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.

    @traversable/zod ships with a bunch of rewriters available off-the-shelf, including:

    @traversable/schema supports other schema libraries too, but they are still being fuzz-tested and aren't ready for production yet.

    Additionally, @traversable/schema publishes its own schema library that's been optimized for AST traversal, and which is documented below.

    @traversable/schema (the package) exploits a TypeScript feature called inferred type predicates to do what libaries like zod do, without the additional runtime overhead or abstraction.

    Note:

    These docs are a W.I.P.

    We recommend jumping straight to the demo or playground.

    The only hard requirement is TypeScript 5.5. Since the core primitive that @traversable/schema is built on top of is inferred type predicates, we do not have plans to backport to previous versions.

    import { t } from '@traversable/schema'

    declare let ex_01: unknown

    if (t.bigint(ex_01)) {
    ex_01
    // ^? let ex_01: bigint
    }

    const schema_01 = t.object({
    abc: t.optional(t.string),
    def: t.tuple(
    t.eq(1),
    t.optional(t.eq(2)), // `t.eq` can be used to match any literal JSON value
    t.optional(t.eq(3)),
    )
    })

    if (schema_01(ex_01)) {
    ex_01
    // ^? let ex_01: { abc?: string, def: [แตƒ: 1, แต‡?: 2, แถœ?: 3] }
    // ^ tuples are labeled to support optionality
    }

    @traversable/schema is modular by schema (like valibot), but takes it a step further by making its feature set opt-in by default.

    The ability to add features like this is a knock-on effect of traversable's extensible core.

    Note: This is the only feature on this list that is built into the core library.

    The motivation for creating another schema library was to add native support for inferred type predicates, which no other schema library currently does (although please file an issue if that has changed!).

    This is possible because the traversable schemas are themselves just type predicates with a few additional properties that allow them to also be used for reflection.

    • Instructions: To use this feature, define a predicate inline and @traversable/schema will figure out the rest.

    You can play with this example in the TypeScript Playground.

    import { t } from '@traversable/schema'

    export let Classes = t.object({
    promise: (v) => v instanceof Promise,
    set: (v) => v instanceof Set,
    map: (v) => v instanceof Map,
    weakMap: (v) => v instanceof WeakMap,
    date: (v) => v instanceof Date,
    regex: (v) => v instanceof RegExp,
    error: (v) => v instanceof Error,
    typeError: (v) => v instanceof TypeError,
    syntaxError: (v) => v instanceof SyntaxError,
    buffer: (v) => v instanceof ArrayBuffer,
    readableStream: (v) => v instanceof ReadableStream,
    })

    type Classes = t.typeof<typeof Classes>
    // ^? type Classes = {
    // promise: Promise<any>
    // set: Set<any>
    // map: Map<any, any>
    // weakMap: WeakMap<object, any>
    // date: Date
    // regex: RegExp
    // error: Error
    // typeError: TypeError
    // syntaxError: SyntaxError
    // buffer: ArrayBuffer
    // readableStream: ReadableStream<any>
    // }

    let Values = t.object({
    function: (v) => typeof v === 'function',
    successStatus: (v) => v === 200 || v === 201 || v === 202 || v === 204,
    clientErrorStatus: (v) => v === 400 || v === 401 || v === 403 || v === 404,
    serverErrorStatus: (v) => v === 500 || v === 502 || v === 503,
    teapot: (v) => v === 418,
    true: (v) => v === true,
    false: (v) => v === false,
    mixed: (v) => Array.isArray(v) || v === true,
    startsWith: (v): v is `bill${string}` => typeof v === 'string' && v.startsWith('bill'),
    endsWith: (v): v is `${string}murray` => typeof v === 'string' && v.endsWith('murral'),
    })

    type Values = t.typeof<typeof Values>
    // ^? type Values = {
    // function: Function
    // successStatus: 200 | 201 | 202 | 204
    // clientErrorStatus: 400 | 401 | 403 | 404
    // serverErrorStatus: 500 | 502 | 503
    // teapot: 418
    // true: true
    // false: false
    // mixed: true | any[]
    // startsWith: `bill${string}`
    // endsWith: `${string}murray`
    // }

    let Shorthand = t.object({
    nonnullable: Boolean,
    unknown: () => true,
    never: () => false,
    })

    type Shorthand = t.typeof<typeof Shorthand>
    // ^? type Shorthand = {
    // nonnullable: {}
    // unknown: unknown
    // never?: never
    // }

    .validate is similar to z.safeParse, except more than an order of magnitude faster*.

    • Instructions: To install the .validate method to all schemas, simply import @traversable/schema-to-validator/install.
    • [ ] TODO: add benchmarks + write-up

    Play with this example in the TypeScript playground.

    import { t } from '@traversable/schema'
    import '@traversable/schema-to-validator/install'
    // โ†‘โ†‘ importing `@traversable/schema-to-validator/install` adds `.validate` to all schemas

    let schema_01 = t.object({
    product: t.object({
    x: t.integer,
    y: t.integer
    }),
    sum: t.union(
    t.tuple(t.eq(0), t.integer),
    t.tuple(t.eq(1), t.integer),
    ),
    })

    let result = schema_01.validate({ product: { x: null }, sum: [2, 3.141592]})
    // โ†‘โ†‘ .validate is available

    console.log(result)
    // =>
    // [
    // { "kind": "TYPE_MISMATCH", "path": [ "product", "x" ], "expected": "number", "got": null },
    // { "kind": "REQUIRED", "path": [ "product" ], "msg": "Missing key 'y'" },
    // { "kind": "TYPE_MISMATCH", "path": [ "sum", 0 ], "expected": 0, "got": 2 },
    // { "kind": "TYPE_MISMATCH", "path": [ "sum", 1 ], "expected": "number", "got": 3.141592 },
    // { "kind": "TYPE_MISMATCH", "path": [ "sum", 0 ], "expected": 1, "got": 2 },
    // { "kind": "TYPE_MISMATCH", "path": [ "sum", 1 ], "expected": "number", "got": 3.141592 },
    // ]

    One of @traversable/schema's primary goals is to remove as much friction from the code generation / metaprogramming workflow as possible.

    To support that goal, all schemas shipped by the @traversable/schema package come with a .toString method that, when called, will return the schema as code.

    This is also useful if you're ever in a situation where you're working with generated schemas, and you need to trouble shoot.

    import { t } from '@traversable/schema'

    const CreateTodoAction = t.object({ type: t.eq('CREATE_TODO') })
    const DeleteTodoAction = t.object({ type: t.eq('DELETE_TODO'), id: t.integer })
    const TodoAction = t.union(
    CreateTodoAction,
    DeleteTodoAction,
    )

    console.log(TodoAction + '')
    // => t.union(t.object({ type: t.eq('CREATE_TODO') }), t.object({ type: t.eq('DELETE_TODO'), id: t.integer }))

    The .toType method prints a stringified version of the type that the schema represents.

    Works on both the term- and type-level.

    • Instructions: To install the .toType method on all schemas, simply import @traversable/schema-to-string/install.

    • Caveat: type-level functionality is provided as a heuristic only; since object keys are unordered in the TS type system, the order that the keys are printed at runtime might differ from the order they appear on the type-level.

    Play with this example in the TypeScript playground

    import { t } from '@traversable/schema'
    import '@traversable/schema-to-string/install'
    // โ†‘โ†‘ importing `@traversable/schema-to-string/install` adds the upgraded `.toType` method on all schemas

    const schema_02 = t.intersect(
    t.object({
    bool: t.optional(t.boolean),
    nested: t.object({
    int: t.integer,
    union: t.union(t.tuple(t.string), t.null),
    }),
    key: t.union(t.string, t.symbol, t.number),
    }),
    t.object({
    record: t.record(t.string),
    maybeArray: t.optional(t.array(t.string)),
    enum: t.enum('x', 'y', 1, 2, null),
    }),
    )

    let ex_02 = schema_02.toType()
    // ^? let ex_02: "({
    // 'bool'?: (boolean | undefined),
    // 'nested': { 'int': number, 'union': ([string] | null) },
    // 'key': (string | symbol | number) }
    // & {
    // 'record': Record<string, string>,
    // 'maybeArray'?: ((string)[] | undefined),
    // 'enum': 'x' | 'y' | 1 | 2 | null
    // })"
    • Instructions: To install the .toJsonSchema method on all schemas, simply import @traversable/schema-to-json-schema/install.

    Play with this example in the TypeScript playground.

    import * as vi from 'vitest'

    import { t } from '@traversable/schema'
    import '@traversable/schema-to-json-schema/install'
    // โ†‘โ†‘ importing `@traversable/schema-to-json-schema/install` adds `.toJsonSchema` on all schemas

    const schema_02 = t.intersect(
    t.object({
    stringWithMaxExample: t.optional(t.string.max(255)),
    nestedObjectExample: t.object({
    integerExample: t.integer,
    tupleExample: t.tuple(
    t.eq(1),
    t.optional(t.eq(2)),
    t.optional(t.eq(3)),
    ),
    }),
    stringOrNumberExample: t.union(t.string, t.number),
    }),
    t.object({
    recordExample: t.record(t.string),
    arrayExample: t.optional(t.array(t.string)),
    enumExample: t.enum('x', 'y', 1, 2, null),
    }),
    )

    vi.assertType<{
    allOf: [
    {
    type: "object"
    required: ("nestedObjectExample" | "stringOrNumberExample")[]
    properties: {
    stringWithMaxExample: { type: "string", minLength: 3 }
    stringOrNumberExample: { anyOf: [{ type: "string" }, { type: "number" }] }
    nestedObjectExample: {
    type: "object"
    required: ("integerExample" | "tupleExample")[]
    properties: {
    integerExample: { type: "integer" }
    tupleExample: {
    type: "array"
    minItems: 1
    maxItems: 3
    items: [{ const: 1 }, { const: 2 }, { const: 3 }]
    additionalItems: false
    }
    }
    }
    }
    },
    {
    type: "object"
    required: ("recordExample" | "enumExample")[]
    properties: {
    recordExample: { type: "object", additionalProperties: { type: "string" } }
    arrayExample: { type: "array", items: { type: "string" } }
    enumExample: { enum: ["x", "y", 1, 2, null] }
    }
    }
    ]
    }>(schema_02.toJsonSchema())
    // โ†‘โ†‘ importing `@traversable/schema-to-json-schema` installs `.toJsonSchema`
    • Instructions: to install the .codec method on all schemas, all you need to do is import @traversable/schema-codec.
      • To create a covariant codec (similar to zod's .transform), use .codec.pipe
      • To create a contravariant codec (similar to zod's .preprocess), use .codec.extend (WIP)

    Play with this example in the TypeScript playground.

    import { t } from '@traversable/schema'
    import '@traversable/schema-codec/install'
    // โ†‘โ†‘ importing `@traversable/schema-codec/install` adds `.codec` on all schemas

    let User = t
    .object({ name: t.optional(t.string), createdAt: t.string })
    .codec // <-- notice we're pulling off the `.codec` property
    .pipe((user) => ({ ...user, createdAt: new Date(user.createdAt) }))
    .unpipe((user) => ({ ...user, createdAt: user.createdAt.toISOString() }))

    let fromAPI = User.parse({ name: 'Bill Murray', createdAt: new Date().toISOString() })
    // ^? let fromAPI: Error | { name?: string, createdAt: Date}

    if (fromAPI instanceof Error) throw fromAPI
    fromAPI
    // ^? { name?: string, createdAt: Date }

    let toAPI = User.encode(fromAPI)
    // ^? let toAPI: { name?: string, createdAt: string }

    Namespaces

    Either
    Equal
    Functor
    Kind
    Mut
    Predicate
    recurse
    SchemaOptions
    t
    t_array
    t_bigint
    t_enum
    t_eq
    t_integer
    t_intersect
    t_number
    t_object
    t_of
    t_optional
    t_record
    t_string
    t_tuple
    t_union
    Type
    TypeConstructor
    TypeError
    UnionToTuple

    Interfaces

    Comparator
    Const
    Functor
    GlobalConfig
    HKT
    newtype
    SchemaConfig
    t_any
    t_array
    t_bigint
    t_boolean
    t_enum
    t_eq
    t_integer
    t_intersect
    t_LowerBound
    t_never
    t_null
    t_number
    t_object
    t_of
    t_optional
    t_record
    t_Schema
    t_string
    t_symbol
    t_tuple
    t_undefined
    t_union
    t_unknown
    t_void
    Tuple
    TypeError
    Typeguard

    Type Aliases

    Algebra
    Atoms
    Coalgebra
    Conform
    Entries
    Equal
    Force
    Guard
    Identity
    IndexedAlgebra
    IndexedRAlgebra
    inline
    Intersect
    Join
    Kind
    Mut
    Mutable
    NonUnion
    Param
    Predicate
    Primitive
    RAlgebra
    Returns
    SchemaOptions
    Showable
    TypeName
    UnionToIntersection
    UnionToTuple
    VERSION

    Variables

    defaultIndex
    defaults
    t_any
    t_bigint
    t_boolean
    t_integer
    t_never
    t_null
    t_number
    t_string
    t_symbol
    t_undefined
    t_unknown
    t_void
    VERSION

    Functions

    applyOptions
    clone
    configure
    get
    get$
    getConfig
    t_array
    t_enum
    t_eq
    t_intersect
    t_object
    t_of
    t_optional
    t_record
    t_tuple
    t_union

    References

    __trimย โ†’ย trim