
Zod实战手册TypeScript数据验证从0到生产级的5个核心模式【免费下载链接】zodTypeScript-first schema validation with static type inference项目地址: https://gitcode.com/GitHub_Trending/zo/zod凌晨两点上游服务把age字段从数字改成了字符串你的接口直接崩了。TypeScript 的类型只在编译期存在运行时它根本不在场。Zod 解决的就是这件事一份声明式的模式定义同时完成运行时数据验证和模式推断让unknown数据进来之前先过一遍安检。30秒上手一个schema替代所有if-elseimport { z } from zod; // 声明模式同时是运行时验证器和 TypeScript 类型来源 const User z.object({ name: z.string().min(1), age: z.number().int().min(0), email: z.string().email().optional(), }); const result User.safeParse({ name: Tom, age: 25 }); if (result.success) { const u: { name: string; age: number; email?: string } result.data; // 类型由模式自动推断你一行类型都没手写 }这一份声明替代了手写的typeof检查、边界判断和类型断言而且z.infertypeof User永远和运行时行为保持一致。设计思想验证引擎与API皮的分层为什么选分层而不是把验证逻辑写死在链式API里看上图Zod 把校验引擎核心基类和链式 APIoptional()、nullable()这类皮拆开了你用的ZodString继承自两者。这样做的实际收益zod/mini能独立提供最小 API编译期优化、JSON Schema 导出这些能力可以单独挂在引擎上而不是让每种组合都重复造一遍轮子。上图是数据流的三个入口parse()处理完全不可信的unknown输入decode()验证已知符合输入类型的值encode()反向把输出值编码回输入格式。后面讲跨服务契约时会用到这个区分。输入校验在函数边界拦截脏数据import { z } from zod; const Register z.object({ // 单字段约束链长度 字符集 username: z.string().min(3).max(20).regex(/^[a-zA-Z0-9_]$/), password: z.string().min(8), // 确认密码字段 confirmPassword: z.string(), }) // 跨字段校验检查两次密码一致失败时把错误挂到具体字段上 .refine((d) d.password d.confirmPassword, { message: 两次输入的密码不一致, path: [confirmPassword], }); export function register(input: unknown) { const r Register.safeParse(input); if (!r.success) return { ok: false as const, issues: r.error.issues }; return { ok: true as const, data: r.data }; // r.data 已收窄为验证后的类型 }容易踩的坑check()只能作用于当前字段跨字段必须用refine()refine的path参数决定错误挂在哪个字段下不传就挂在对象根上前端表单定位不到输入框。多态响应用discriminatedUnion区分结构import { z } from zod; // 判别联合靠 type 字段区分不同分支验证时按判别值直接命中分支 const ApiResponse z.discriminatedUnion(type, [ z.object({ type: z.literal(success), data: z.object({ id: z.string() }) }), z.object({ type: z.literal(error), code: z.number(), message: z.string() }), ]); function handle(res: unknown) { const r ApiResponse.safeParse(res); if (!r.success) throw new Error(JSON.stringify(r.error.issues)); // 类型被收窄为两个分支的联合按 r.data.type 分发即可 return r.data; }容易踩的坑所有分支必须包含判别字段且是字面量否则类型系统会直接报错——别用.union()硬凑多态它要逐个候选试discriminatedUnion是 O(1) 命中。跨服务契约codec双向验证输入和输出import { z } from zod; // 输入侧数据库风格 snake_case const dbShape z.object({ user_id: z.string(), full_name: z.string(), created_at: z.string().datetime(), }); // 输出侧应用风格 camelCase const appShape z.object({ userId: z.string(), fullName: z.string(), createdAt: z.coerce.date(), // 解析时字符串转 Date }); // 双向编解码器decode 校验输入再转换encode 反向做同样严格的验证 const UserCodec z.codec(dbShape, appShape, { decode: (d) ({ userId: d.user_id, fullName: d.full_name, createdAt: d.created_at, }), encode: (a) ({ user_id: a.userId, full_name: a.fullName, created_at: a.createdAt.toISOString(), }), }); const row: unknown { user_id: u1, full_name: Tom, created_at: 2025-01-01T00:00:00Z }; const app UserCodec.decode(row); // 入库方向unknown → 应用类型 const back UserCodec.encode(app); // 写库方向应用类型 → 入库格式容易踩的坑encode()不是简单赋值它会对输出侧 schema 做完整验证——如果输出 schema 里有个必填字段你忘了填encode 会直接抛错。这正是它比手写映射安全的地方但前提是两侧 schema 都写全。性能敏感路径用compile把解释执行换成生成代码import { z } from zod; import zod/compile; // 副作用导入此后所有 schema 首次 parse 时自动编译 // 也可显式编译单个 schema生成一段针对该 schema 的专用校验代码 const compiledUser z.compile( z.object({ name: z.string(), age: z.number().int().min(0) }) ); // 热路径上反复调用生成代码没有逐节点解释开销 compiledUser.parse({ name: Tom, age: 25 });容易踩的坑编译产物依赖运行时生成代码CSP 禁用 eval 的环境会失败仓库里专门有 jitless 测试 覆盖这个回退路径异步refine等无法静态生成的逻辑会自动降级回普通解析器不报错也不加速。选型与决策什么时候用Zod什么时候不用维度ZodValibotArkTypeTypeBox包体积mingzip约10KB可配mini更省更省较大较小核心风格声明式模式推断函数式组合声明式类型推断JSON Schema 优先JSON Schema 导出内置需插件内置原生编译期提速内置compile无有有明确建议团队已经用 TypeScript、重视一份声明两用默认选 Zod只发服务端、追求极限 bundle 体积选 Valibot后端契约本来就以 JSON Schema 为单一事实来源选 TypeBox。别为了 5KB 体积放弃类型推断——那种场景你大概率本来也该用 mini。进阶路径从换写法到改行为换个写法——全量用着没问题但包体积敏感把 import 换成 mini 包import { z } from zod/mini; // 去掉 JSON Schema、locales 等外围能力的轻量出口 const User z.object({ name: z.string(), age: z.number().int() }); User.parse({ name: Tom, age: 25 });改内部行为——默认错误文案是英文不想每个字段单独传 message 时全局设一次 localeimport { z } from zod; import { $ZodError } from zod; import { zhCN } from zod/locales; // 全局错误文案换成中文后续所有 schema 共用 z.globalConfig($ZodError, { locale: zhCN });接入工作流——tRPC 里 schema 同时约束输入和输出两端类型自动贯穿import { initTRPC } from trpc/server; import { z } from zod; const t initTRPC.create(); export const appRouter t.router({ user: t.procedure .input(z.object({ id: z.string() })) // 输入校验 .output(z.object({ id: z.string(), name: z.string() })) // 输出契约 .query(async ({ input }) db.getUser(input.id)), });如果你需要把 schema 交换给非 TS 生态的同事toJSONSchema()可以一键导出 JSON Schema反向的fromJSONSchema()也在 from-json-schema 模块 里。如果你怀疑自己的用法会引入内存放大仓库里有一组差分测试可以直接跑codec 示例、compile 差分测试 都是现成参照。下次告警再响把 schema 挂在接口入口处跑一遍safeParse再照着 compile 差分测试把热路径编译掉。仓库本地探索入口核心实现。git clone https://gitcode.com/GitHub_Trending/zo/zod 即可拉取完整仓库对照阅读。【免费下载链接】zodTypeScript-first schema validation with static type inference项目地址: https://gitcode.com/GitHub_Trending/zo/zod创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考