UNPKG

zod-to-ts

Version:

generate TypeScript types from your Zod schema

326 lines (325 loc) 14.1 kB
import ts, { factory as f } from 'typescript'; import { addJsDocComment, createTypeReferenceFromString, getIdentifierOrStringLiteral, } from "./ast-helpers.js"; import { resolveOptions, } from "./types.js"; import { handleUnrepresentable, literalValueToLiteralType } from "./utils.js"; function callTypeOverride(schema, options) { const getTypeOverride = options.overrides?.get(schema); if (getTypeOverride) { return getTypeOverride(ts, options); } else if (options.overrideFunction) { return options.overrideFunction(schema, ts, options); } else { return undefined; } } // This only applies to wrapper types function hasInnerType(def) { return 'innerType' in def; } function getDescriptionFromSchema(schema, options) { const directDescription = options.metadataRegistry?.get(schema)?.description; if (directDescription) return directDescription; if (hasInnerType(schema._zod.def)) { return options.metadataRegistry?.get(schema._zod.def.innerType)?.description; } return undefined; } function withAuxiliaryType(schema, getInner, options) { const auxiliaryTypeDefinition = { identifier: f.createIdentifier(options.auxiliaryTypeStore.nextId()), }; const internalDefinitionsMap = options.auxiliaryTypeStore.definitions; internalDefinitionsMap.set(schema, auxiliaryTypeDefinition); const node = getInner(); if (auxiliaryTypeDefinition.used) { auxiliaryTypeDefinition.node = f.createTypeAliasDeclaration(undefined, auxiliaryTypeDefinition.identifier, undefined, node); return f.createTypeReferenceNode(auxiliaryTypeDefinition.identifier); } internalDefinitionsMap.delete(schema); return node; } export function zodToTs(zod, options) { const resolvedOptions = resolveOptions(options); const node = zodToTsNode(zod, resolvedOptions); return { node }; } function zodToTsNode(schema, options) { const def = schema._zod.def; const typeOverride = callTypeOverride(schema, options); if (typeOverride) return typeOverride; const auxiliaryTypeDefinition = options.auxiliaryTypeStore.definitions.get(schema); if (auxiliaryTypeDefinition) { ; auxiliaryTypeDefinition.used = true; return f.createTypeReferenceNode(auxiliaryTypeDefinition.identifier); } switch (def.type) { case 'string': { return f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword); } case 'nan': case 'number': { return f.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword); } case 'bigint': { return f.createKeywordTypeNode(ts.SyntaxKind.BigIntKeyword); } case 'success': case 'boolean': { return f.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword); } case 'date': { return createTypeReferenceFromString('Date'); } case 'undefined': { return f.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword); } case 'null': { return f.createLiteralTypeNode(f.createNull()); } case 'void': { return f.createUnionTypeNode([ f.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword), f.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword), ]); } case 'any': { return f.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword); } case 'unknown': { return f.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword); } case 'never': { return f.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword); } case 'lazy': { return withAuxiliaryType(schema, () => zodToTsNode(def.getter(), options), options); } case 'literal': { // z.literal(['hi', 'bye']) -> 'hi' | 'bye' const members = def.values.map((value) => literalValueToLiteralType(value)); if (members.length === 1) { return members[0]; } return f.createUnionTypeNode(members); } case 'object': { return withAuxiliaryType(schema, () => { const properties = Object.entries(def.shape); const members = properties.map(([key, memberZodSchema]) => { const type = zodToTsNode(memberZodSchema, options); const isOptional = options.io === 'input' ? memberZodSchema._zod.optin : memberZodSchema._zod.optout; const propertySignature = f.createPropertySignature(undefined, getIdentifierOrStringLiteral(key), isOptional ? f.createToken(ts.SyntaxKind.QuestionToken) : undefined, type); const description = getDescriptionFromSchema(memberZodSchema, options); if (description) { addJsDocComment(propertySignature, description); } return propertySignature; }); if (def.catchall) { const catchallType = zodToTsNode(def.catchall, options); members.push(f.createIndexSignature(undefined, [ f.createParameterDeclaration(undefined, undefined, f.createIdentifier('x'), undefined, f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)), ], catchallType)); } return f.createTypeLiteralNode(members); }, options); } case 'array': { const type = zodToTsNode(def.element, options); const node = f.createArrayTypeNode(type); return node; } case 'enum': { // z.enum(['a', 'b', 'c']) -> 'a' | 'b' | 'c const members = Object.values(def.entries).map((value) => literalValueToLiteralType(value)); return f.createUnionTypeNode(members); } case 'union': { // z.union([z.string(), z.number()]) -> string | number const types = def.options.map((option) => zodToTsNode(option, options)); return f.createUnionTypeNode(types); } case 'optional': { const innerType = zodToTsNode(def.innerType, options); return f.createUnionTypeNode([ innerType, f.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword), ]); } case 'nullable': { const innerType = zodToTsNode(def.innerType, options); return f.createUnionTypeNode([ innerType, f.createLiteralTypeNode(f.createNull()), ]); } case 'tuple': { // z.tuple([z.string(), z.number()]) -> [string, number] const types = def.items.map((option) => zodToTsNode(option, options)); return f.createTupleTypeNode(types); } case 'record': { // z.record(z.number()) -> { [x: string]: number } const keyType = zodToTsNode(def.keyType, options); const valueType = zodToTsNode(def.valueType, options); const node = f.createTypeLiteralNode([ f.createIndexSignature(undefined, [ f.createParameterDeclaration(undefined, undefined, f.createIdentifier('key'), undefined, keyType), ], valueType), ]); return node; } case 'map': { // z.map(z.string()) -> Map<string> const keyType = zodToTsNode(def.keyType, options); const valueType = zodToTsNode(def.valueType, options); const node = f.createTypeReferenceNode(f.createIdentifier('Map'), [ keyType, valueType, ]); return node; } case 'set': { // z.set(z.string()) -> Set<string> const type = zodToTsNode(def.valueType, options); const node = f.createTypeReferenceNode(f.createIdentifier('Set'), [type]); return node; } case 'intersection': { // z.number().and(z.string()) -> number & string const left = zodToTsNode(def.left, options); const right = zodToTsNode(def.right, options); const node = f.createIntersectionTypeNode([left, right]); return node; } case 'promise': { // z.promise(z.string()) -> Promise<string> const type = zodToTsNode(def.innerType, options); const node = f.createTypeReferenceNode(f.createIdentifier('Promise'), [ type, ]); return node; } case 'function': { // z.function({ input: [z.string()], output: z.number() }) -> (args_0: string) => number const argumentSchemas = def.input._zod.def.items; const parameterTypes = argumentSchemas.map((argument, index) => { const parameterType = zodToTsNode(argument, options); return f.createParameterDeclaration(undefined, undefined, f.createIdentifier(`args_${index}`), undefined, parameterType); }); const returnType = zodToTsNode(def.output, options); const node = f.createFunctionTypeNode(undefined, parameterTypes, returnType); return node; } case 'prefault': case 'default': { // z.string().optional().default('hi') -> string // if it's an input type, the type is optional if (options.io === 'input') { } return zodToTsNode(def.innerType, options); } case 'file': { return createTypeReferenceFromString('File'); } case 'pipe': { const innerType = options.io === 'input' ? def.in._zod.def.type === 'transform' ? def.out : def.in : def.out; return zodToTsNode(innerType, options); } case 'readonly': { const innerType = zodToTsNode(def.innerType, options); if (ts.isArrayTypeNode(innerType) || ts.isTupleTypeNode(innerType)) { return f.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, innerType); } if (ts.isTypeLiteralNode(innerType)) { const members = innerType.members.map((member) => { if (ts.isPropertySignature(member)) { return f.updatePropertySignature(member, [ ...(member.modifiers ?? []), f.createToken(ts.SyntaxKind.ReadonlyKeyword), ], member.name, member.questionToken, member.type); } return member; }); return f.createTypeLiteralNode(members); } if (ts.isTypeReferenceNode(innerType) && ts.isIdentifier(innerType.typeName)) { const identifier = innerType.typeName.text; if (identifier === 'Set') return f.createTypeReferenceNode(f.createIdentifier('ReadonlySet'), innerType.typeArguments); if (identifier === 'Map') return f.createTypeReferenceNode(f.createIdentifier('ReadonlyMap'), innerType.typeArguments); } // fall back to just returning the inner type return innerType; } case 'catch': { // z.string().catch('default') -> string return zodToTsNode(def.innerType, options); } case 'nonoptional': { // z.string().optional().nonoptional() -> Exclude<string | undefined, undefined> -> string return f.createTypeReferenceNode('Exclude', [ zodToTsNode(def.innerType, options), f.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword), ]); } case 'symbol': { return f.createKeywordTypeNode(ts.SyntaxKind.SymbolKeyword); } case 'template_literal': { const partNodes = def.parts.map((part) => { if (typeof part !== 'object' || part === null) { return literalValueToLiteralType(part); } return zodToTsNode(part, options); }); let templateHead; const templateSpans = []; let currentTypeSpanNode; let currentMiddle = ''; for (const node of partNodes) { if (ts.isLiteralTypeNode(node) && ts.isStringLiteral(node.literal)) { currentMiddle += node.literal.text; continue; } if (currentTypeSpanNode) { const templateSpan = f.createTemplateLiteralTypeSpan(currentTypeSpanNode, f.createTemplateMiddle(currentMiddle)); templateSpans.push(templateSpan); } else { // if it's the first, set the head templateHead = f.createTemplateHead(currentMiddle); currentTypeSpanNode = node; } currentMiddle = ''; currentTypeSpanNode = node; } if (templateHead && currentTypeSpanNode) { const templateSpan = f.createTemplateLiteralTypeSpan(currentTypeSpanNode, f.createTemplateTail(currentMiddle)); templateSpans.push(templateSpan); } else { return f.createLiteralTypeNode(f.createNoSubstitutionTemplateLiteral(currentMiddle)); } return f.createTemplateLiteralType(templateHead, templateSpans); } } return handleUnrepresentable(options.unrepresentable, def.type); } export { createTypeAlias, printNode } from "./ast-helpers.js"; export { createAuxiliaryTypeStore } from "./utils.js";