Zod 数据校验指南:5 分钟跑通 TypeScript 类型推断,从入门到进阶
【免费下载链接】zodTypeScript-first schema validation with static type inference项目地址: https://gitcode.com/GitHub_Trending/zo/zod
你从前端拿到一份用户提交,却不确定字段是否齐全、类型是否正确。与其手写一堆if去逐个检查,Zod 让你声明一个 schema,再调用一次.parse(),就同时拿到运行时校验结果和静态类型推断。下面带你先 5 分钟跑通第一个例子,再逐层拆开它的核心机制,最后覆盖表单校验、API 边界校验等真实场景与常见坑。
五分钟上手 ⏱️
先装再跑。Zod 无外部依赖,一行命令即可安装:
npm install zod最小可运行示例。定义一个对象 schema,再喂给它一份数据:
import * as z from "zod"; const User = z.object({ username: z.string(), xp: z.number(), }); const data = User.parse({ username: "billie", xp: 100 }); console.log(data); // { username: "billie", xp: 100 }这段做了什么:z.object把两个字段登记成一张校验清单,.parse()对输入逐项核对,全部通过才返回值;任何一项不符就抛出ZodError。
第一条成功结果。返回值不只是数据,它还带着 TypeScript 推断出来的类型,你无需再写interface:
type User = z.infer<typeof User>; // { username: string; xp: number } const u: User = { username: "billie", xp: 100 };z.infer从 schema 反向提取类型,schema 一改,类型自动跟着变,这就是"声明一次、校验与类型两用"。
核心机制拆解
结论:一个 schema 既是运行时校验器,也是类型定义。它把"数据长什么样"这件事,写成了唯一的事实来源:
const User = z.object({ username: z.string(), xp: z.number() }); User.parse({ username: "a", xp: 1 }); // 运行时校验 type User = z.infer<typeof User>; // 静态类型校验与类型出自同一份声明,二者不会再出现"接口和校验各写一遍"的漂移。
结论:消费校验结果有两种方式——抛错或返回判别联合。parse失败即抛错,safeParse则把结果装进一个带success标记的对象:
const res = User.safeParse({ username: 42, xp: "100" }); if (!res.success) { res.error.issues; // 每个问题都带 expected、code、path、message } else { res.data; // { username: string; xp: number } }safeParse的结果是判别联合,if (!res.success)就能让编译器自动收窄到错误分支,适合表单这类需要逐字段提示的场景。
结论:输入和输出可以是两种类型。一旦用了.transform(),进入的和出去的数据就不一样,需要分别取:
const Age = z.string().transform((s) => Number(s)); type In = z.input<typeof Age>; // string type Out = z.output<typeof Age>; // number,等价于 z.inferz.infer默认取输出类型;要拿到"进来时"的类型,用z.input。这解释了为什么 transform 后变量类型会"跳变"。
结论:跨字段规则靠 refine 补齐。单字段约束用链式方法,字段之间互相牵制的逻辑用.refine():
const Register = z.object({ password: z.string().min(8), confirm: z.string(), }).refine((d) => d.password === d.confirm, { message: "两次密码不一致", path: ["confirm"], });refine收到的是整个对象,可以任意断言;path让报错精确挂到具体字段上,方便前端定位。
场景实战
场景一:注册表单的多字段校验。需求是把用户名、邮箱、年龄一次校验完,失败时还要能告诉用户错在哪。
import * as z from "zod"; const Signup = z.object({ username: z.string().min(3).max(20).regex(/^[a-zA-Z0-9_]+$/), email: z.string().email(), age: z.number().int().min(18), }); const res = Signup.safeParse({ username: "billie", email: "b@example.com", age: 17, }); if (!res.success) { res.error.issues; // [{ path: ["age"], code: "too_small", ... }] }关键点:
- 链式方法(
min/email/int)就是校验器,顺序即执行顺序。 issues里每条都有path,直接对应表单字段名,渲染错误提示时零转换。- 用
safeParse而非parse,避免把"预期内的失败"变成异常流。
场景二:HTTP API 响应边界校验。微服务里第三方返回值不可信,要在进入业务逻辑前挡一道,但又不想为"只是判断合不合法"就构造完整错误。
import * as z from "zod"; const ApiResp = z.object({ success: z.boolean(), code: z.number().int().min(200).max(599), data: z.object({ id: z.string() }).optional(), }); function isApiResponse(v: unknown): boolean { return z.validate(ApiResp, v); // 只返回 boolean,不构造错误 }关键点:
- 顶层
z.validate(schema, value)只做判断,比safeParse更轻,适合高频边界。 - 需要把
data里的对象进一步收窄时,再对它单独.safeParse拿结构化错误。 - 边界处统一校验,业务代码即可信任
res.data的类型。
场景三:高频路径的 AOT 编译加速。列表、批量导入这类要解析上万条记录的接口,逐节点派发的解析开销会被放大。
import * as z from "zod"; const Row = z.object({ id: z.string(), name: z.string(), age: z.number(), }); const fast = z.compile(Row); // 预编译成扁平、无循环的校验器 fast.parse({ id: "1", name: "a", age: 1 });关键点:
z.compile把逐键遍历展开成可直接执行的校验逻辑,对象/数组这类容器收益最大(仓库基准里大对象约 9 倍)。- 合法输入走快速路径,非法输入回退到常规解析器,报错信息与原版一致。
- 编译是"最终形态"才生效,所以先写完整 schema 再
compile(见下方避坑)。
避坑指南 ⚠️
现象:只想判断"合不合法",却套了try/catch或safeParse。原因是parse会抛错、safeParse会构造完整ZodError,判断合法时这些都做了多余工作。解法:用顶层z.validate(schema, value)直接拿布尔值,含异步 refine 时用z.validateAsync。
现象:对 schema 先.refine()再z.compile(),性能没提升。原因是.refine()、.extend()这类派生方法返回的是未编译的新 schema,compile只作用于它传入的那一份。解法:把编译放在最外层,即z.compile(base.refine(...)),而非z.compile(base).refine(...)。
现象:transform之后给变量赋值报"类型不匹配"。原因是输入类型和输出类型被分离了,而z.infer取的是输出。解法:表示"进入 schema 前"的数据用z.input<typeof Schema>,表示"出去后"的用z.infer或z.output。
现象:含asyncrefine 的 schema 调.parse()结果不对或不生效。原因是异步校验需要被等待,同步parse无法拿到 Promise 的结果。解法:改用.parseAsync()/.safeParseAsync(),并await它。
周边联动 🔗
与 React Hook Form 集成。表单库提供了 Zod 的 resolver,schema 直接当校验源,错误字段自动对应到表单字段:
import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import * as z from "zod"; const form = z.object({ username: z.string().min(3) }); const { register, handleSubmit } = useForm({ resolver: zodResolver(form), });与 JSON Schema 互转。需要把校验规则交给别的系统时,Zod 内置了双向转换,schema 到 JSON Schema 用toJSONSchema,反向用fromJSONSchema,实现见 json-schema 处理源码。
收尾
Zod 的核心价值在于把"校验"和"类型"收敛到同一份声明里:parse给出类型安全的数据,safeParse给出可渲染的错误,compile在高频路径上省掉重复派发。理解了 schema 即事实来源这一点,剩下都是 API 的取舍。想继续深挖,可以从这几处入手:
- 基础用法(解析、报错、类型推断):基础用法文档
- AOT 编译的原理与限制:编译说明
- 经典 API 的核心实现:classic 源码目录
- 覆盖校验行为的完整用例:测试用例目录
【免费下载链接】zodTypeScript-first schema validation with static type inference项目地址: https://gitcode.com/GitHub_Trending/zo/zod
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考