zod-to-ts

repository·main·Indexed 19 days ago

https://github.com/sachinraja/zod-to-ts

A utility for generating TypeScript AST nodes and type declarations from Zod schemas. It enables synchronization between runtime validation and compile-time types, supporting features like input/output type extraction, custom type overrides via TypeOverrideMap, and the management of recursive schemas through an auxiliaryTypeStore. Requires zod@4 and typescript@5 as peer dependencies.

Tokens
3.4K
Snippets
11
Records
13
Agent score
65%

What's inside zod-to-ts

  1. How auxiliary types and auxiliaryTypeStore work

    main

    Recursive schemas (e.g., a category containing an array of categories) cannot be represented by a single type declaration. zod-to-ts uses an auxiliaryTypeStore to manage these helper types.

    When a recursive or complex type is encountered, zodToTs returns a reference node (e.g., Auxiliary_0) and stores the actual type definition in the auxiliaryTypeStore. To generate a complete file, you must extract the definitions from the store and prepend them to your main type declaration.

    import { z } from 'zod'
    import { createAuxiliaryTypeStore, zodToTs, createTypeAlias, printNode } from 'zod-to-ts'
    
    const Category = z.object({
      name: z.string(),
      get subcategories() {
        return z.array(Category)
      }
    })
    
    const auxiliaryTypeStore = createAuxiliaryTypeStore()
    const { node } = zodToTs(Category, { auxiliaryTypeStore })
    
    // 1. Extract all auxiliary definitions as a string preamble
    const auxiliaryTypePreamble = auxiliaryTypeStore.definitions
    	.values()
    	.toArray()
    	.map((definition) => printNode(definition.node))
    	.join('\n')
    
    // 2. Create the main type alias
    const categoryTypeAlias = createTypeAlias(node, 'Category')
    const categoryType = printNode(categoryTypeAlias)
    
    // 3. Combine them
    const outputFile = `${auxiliaryTypePreamble}\n${categoryType}`
    console.log(outputFile)
  2. Generate TypeScript types from Zod schemas

    main

    Use zodToTs to convert a Zod schema into a TypeScript AST node. The returned node represents the type but is not a full type declaration string. To create a full type declaration, use createTypeAlias. To convert the node to a string, use printNode.

    import { z } from 'zod'
    import { zodToTs, createTypeAlias, printNode } from 'zod-to-ts'
    
    const UserSchema = z.object({
    	username: z.string(),
    	age: z.number(),
    	inventory: z.object({
    		name: z.string(),
    		itemId: z.number(),
    	}).array(),
    })
    
    const { node } = zodToTs(UserSchema)
    
    // Create a type alias declaration
    const typeAlias = createTypeAlias(node, 'User')
    
    // Convert the node/alias to a string
    const nodeString = printNode(typeAlias)
    console.log(nodeString)
  3. Configure zodToTs via ZodToTsOptions

    main

    The zodToTs function accepts a ZodToTsOptions object to customize the conversion process. Key configuration capabilities include:

    • io: Determines whether to generate types for the input (data entering the schema) or output (data after transformations/refinements). This affects how optionality and transformations are handled.
    • overrides: A Map or object that allows you to provide a TypeOverrideFunction for specific Zod schemas. This is useful when you want a specific schema to map to a custom TypeScript type instead of the default derivation.
    • overrideFunction: A fallback function that can be called for any schema that doesn't have a specific entry in overrides.
    • metadataRegistry: Used to retrieve descriptions or other metadata from schemas to be converted into JSDoc comments.
    • unrepresentable: A strategy for handling Zod types that cannot be directly mapped to TypeScript types.
    • auxiliaryTypeStore: Manages the creation of auxiliary type aliases (e.g., for z.lazy or complex objects) to avoid deep nesting or circularity issues.
  4. Override Zod to TypeScript mappings

    main

    You can customize how specific Zod schemas are converted to TypeScript types using two methods in ZodToTsOptions:

    1. overrides: A TypeOverrideMap (a Map of Zod schemas to TypeOverrideFunction) that provides direct, high-priority mappings for specific schemas.
    2. overrideFunction: An OptionalTypeOverrideFunction that acts as a fallback. It is called for Zod types that are not matched in the overrides map. If it returns undefined, the default mapping is used.

    Both methods receive the typescript compiler interface and the current options to allow for AST node creation.

    // Example of an override function signature
    type TypeOverrideFunction = (
    	typescript: typeof ts,
    	options: ZodToTsOptions,
    ) => ts.TypeNode;
    
    // Example of an optional override function signature
    type OptionalTypeOverrideFunction = (
    	schema: z4.$ZodType,
    	typescript: typeof ts,
    	options: ZodToTsOptions,
    ) => ts.TypeNode | undefined;
  5. Configure zodToTs options

    main

    The zodToTs function accepts an optional configuration object as its second argument. Key options include:

    • metadataRegistry: Used for extracting schema metadata (like description) into JSDoc comments. Defaults to the global registry.
    • unrepresentable: Determines behavior when encountering non-statically analyzable APIs like z.transform() or z.custom(). Set to 'any' to return any instead of throwing an error.
    • io: Specifies whether to extract the schema's 'input' or 'output' type. Defaults to 'output'.
    zodToTs(schema, {
    	metadataRegistry: myRegistry,
    	unrepresentable: 'any',
    	io: 'input'
    })
  6. Override types using TypeOverrideMap

    main

    If zod-to-ts cannot statically infer a type (for example, with z.instanceof(Date)), you can provide a manual override. Use TypeOverrideMap to map a Zod schema to a custom TypeScript AST node via a callback function.

    import { z } from 'zod'
    import { createAuxiliaryTypeStore, type TypeOverrideMap, zodToTs } from 'zod-to-ts'
    
    const overrides: TypeOverrideMap = new Map()
    
    // Manually define how DateSchema should be represented in TS
    const DateSchema = z.instanceof(Date)
    overrides.set(DateSchema, (ts) =>
    	s.factory.createTypeReferenceNode(ts.factory.createIdentifier('Date')),
    )
    
    const ItemSchema = z.object({
    	name: z.string(),
    	date: DateSchema,
    })
    
    const auxiliaryTypeStore = createAuxiliaryTypeStore()
    const { node } = zodToTs(ItemSchema, {
    	auxiliaryTypeStore,
    	overrides,
    })
  7. Configure ZodToTsOptions

    main

    When calling zodToTs, you can provide a ZodToTsOptions object to control the output. Key configuration properties include:

    • unrepresentable: Determines behavior when a type cannot be represented in TypeScript.
      • 'throw' (default): Throws an error.
      • 'any': Converts the type to {}.
    • io: Determines whether to extract the input or output type (relevant for transforms, coerced primitives, etc.).
      • 'output' (default): Converts the output schema.
      • 'input': Converts the input schema.
    • overrides: A TypeOverrideMap for specific schema overrides.
    • overrideFunction: A fallback function for schema overrides.
    • metadataRegistry: A registry used to look up metadata (like description) for each schema. Defaults to globalRegistry if not provided.
  8. Handle unrepresentable Zod types

    main

    The handleUnrepresentable function manages how Zod schemas that cannot be directly mapped to TypeScript types are handled. If the unrepresentable option in ZodToTsOptions is set to 'any', it returns a TypeScript any keyword type node. Otherwise, it throws an error indicating that the specific schema type cannot be represented in TypeScript.

    // Example of how the logic behaves based on configuration
    // If unrepresentable: 'any'
    // Result: ts.any
    
    // If unrepresentable is not 'any'
    // Result: Error: Schemas of type "<type>" cannot be represented in TypeScript
  9. Create an AuxiliaryTypeStore

    main

    The createAuxiliaryTypeStore function initializes a new AuxiliaryTypeStore. This store is used to manage and track auxiliary type definitions (types generated during the conversion process that need to be referenced by name). It provides a nextId() method that generates unique identifiers (e.g., Auxiliary_0, Auxiliary_1) and a definitions Map to store the actual type definitions.

    const store = createAuxiliaryTypeStore();
    const id = store.nextId(); // "Auxiliary_0"
    store.definitions.set(id, someTypeNode);
  10. Override TypeScript types for specific Zod schemas

    main

    You can control how specific Zod schemas are converted by using the overrides option. This allows you to map a Zod schema to a specific TypeScript type (like a custom interface or a primitive) that doesn't match the default conversion logic.

    overrides accepts a TypeOverrideMap, which maps a Zod schema to a TypeOverrideFunction.

    import { zodToTs } from 'zod-to-ts';
    import { z } from 'zod';
    import ts from 'typescript';
    
    const mySchema = z.string();
    
    const { node } = zodToTs(mySchema, {
      overrides: new Map([
        [mySchema, (schema, ts) => ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)]
      ])
    });
  11. Convert Zod schemas to TypeScript AST nodes with zodToTs

    main

    The zodToTs function is the primary entry point for converting a Zod schema into a TypeScript Abstract Syntax Tree (AST) node. It accepts a Zod schema and an optional configuration object, returning an object containing the generated node (a ts.TypeNode).

    To use this, you will need the typescript package installed, as the returned node is a native TypeScript AST node.

    import { zodToTs } from 'zod-to-ts';
    import { z } from 'zod';
    
    const schema = z.object({
      name: z.string(),
      age: z.number().optional(),
    });
    
    const { node } = zodToTs(schema, {});
    // 'node' is a ts.TypeNode representing { name: string; age?: number; }