Skip to Content
路漫漫其修远兮,吾将上下而求索

Zod 入门到实践:类型与校验的统一之道

Zod 是一个“运行时数据校验库”,同时也是“类型生成器”。它用一份模式(schema)在运行时验证对象结构,并在编译时为 TypeScript 生成类型。用一句话概括:Zod 让“类型(TS)”和“校验(运行时)”成为同一份真源。

为什么用 Zod

  • 类型与校验统一:用一份模式同时完成 TS 类型推导与运行时验证,不再“声明一个类型、写一堆 if 判断”各自维护。
  • 强健的运行时安全:TS 只在编译期保证类型,Zod 在运行时拦截脏数据(接口返回、表单输入、URL 参数、本地存储)。
  • 组合能力强:支持 object/array/enum/union/literal/optional/nullable/transform/refine 等。
  • 前后端通吃:前端表单、后端接口入口、BFF 校验、中间层数据清洗都适用。

安装

npm install zod # 或 pnpm/yarn 安装

基本用法

  • 定义模式与类型推导
import { z } from 'zod' // 定义模式 const TaskSchema = z.object({ name: z.string().min(1, '任务名称不能为空').max(30, '任务名称不能超过 30 个字符'), description: z.string().max(200, '描述不能超过 200 字').optional(), dueDate: z.string().optional(), priority: z.enum(['low', 'medium', 'high']).optional(), }) // 推导类型 type TaskInput = z.infer<typeof TaskSchema>
  • 两种校验方式
// 失败抛错(异常流) const data = TaskSchema.parse(form) // 安全校验(推荐) const parsed = TaskSchema.safeParse(form) if (!parsed.success) { // parsed.error 是 ZodError const message = parsed.error.errors[0]?.message ?? '校验失败' // 自己决定如何提示用户(字段级、全局提示等) } else { // parsed.data 是类型安全且运行时校验通过的数据 }

组合与常用内置类型

  • z.string().min(n).max(n).email().url()...
  • z.number().int().positive().gte(n).lte(n)...
  • z.boolean()
  • z.enum(['a','b','c'])z.literal('a')
  • z.array(z.string()).min(1)z.tuple([...])
  • z.union([A, B])z.intersection(A, B)
  • z.record(z.string(), z.number())
  • z.object({ ... })z.object({ ... }).partial()(全部改可选)、.required()(全部必填)
  • z.nullable(T)(可为 null)、z.optional(T)(可为 undefined)

示例:

const UserSchema = z.object({ id: z.string().uuid(), email: z.string().email(), tags: z.array(z.string()).default([]), }) type User = z.infer<typeof UserSchema>

自定义规则与跨字段校验

  • 单字段 refine
const PhoneSchema = z.string().refine(v => /^1\d{10}$/.test(v), '手机号格式不正确')
  • 跨字段校验(superRefine
const RangeSchema = z.object({ start: z.number(), end: z.number() }) .superRefine((val, ctx) => { if (val.end < val.start) { ctx.addIssue({ path: ['end'], // 指定错误归属字段 code: z.ZodIssueCode.custom, message: '结束值必须大于开始值', }) } })

转换与预处理

  • transform:校验前后做映射(生成新的数据)
const TrimmedName = z.string().transform(s => s.trim())
  • preprocess:在进入模式之前预处理原始输入
const DateStringToISO = z.preprocess( (v) => typeof v === 'string' ? new Date(v).toISOString() : v, z.string().datetime() )

结构工具:Pick/Omit/Merge

const Base = z.object({ a: z.string(), b: z.number(), c: z.boolean() }) const OnlyA = Base.pick({ a: true }) const WithoutB = Base.omit({ b: true }) const Extended = Base.merge(z.object({ d: z.string() }))

错误处理与展示

  • safeParse 返回的 ZodError 结构化错误:
    • error.errors 是数组,包含 path(字段路径)、messagecode
  • 将错误映射成“字段 → 文案”的字典,便于字段级提示:
const parsed = TaskSchema.safeParse(form) if (!parsed.success) { const fieldErrors = parsed.error.errors.reduce((acc, issue) => { const key = issue.path.join('.') || '_root' acc[key] = issue.message return acc }, {} as Record<string, string>) // setFieldErrors(fieldErrors) 或 setError(...) 全局提示 }

与 TypeScript 的结合

  • Zod 是“类型的实现”,z.infer<typeof Schema> 得到 TS 类型,前后端共享同一份规则。
  • 当 schema 变化时,类型自动更新,避免“类型与校验脱节”。

用于表单校验(与 UI 配合)

  • 控件级轻量约束(如 maxLength)用于即时反馈;
  • 提交前用 Zod 做“最终把关”(统一规则、跨字段校验、转换);
  • React Hook Form 可用 zodResolver 实现“Zod 作为唯一校验器”。

示例(手动校验):

const parsed = TaskSchema.safeParse(form) if (!parsed.success) { setError(parsed.error.errors[0]?.message || '校验失败') return } mutation.mutate({ id: taskId, data: parsed.data })

与 React Query/接口调用的关系

  • React Query 管理请求与缓存;Axios/Fetch 负责网络传输;Zod 在提交前/响应后做数据校验与清洗。
  • 推荐顺序:UI 输入 → Zod 校验 → React Query mutate(内部 axios/fetch)→ 成功后更新状态。
const mutation = useMutation({ mutationFn: async ({ id, data }: { id: string; data: TaskInput }) => { // await axios.patch(`/tasks/${id}`, data) await new Promise(res => setTimeout(res, 300)) // 模拟接口 }, onSuccess: () => { /* 更新状态、关闭抽屉等 */ } }) const onSubmit = () => { const parsed = TaskSchema.safeParse(form) if (!parsed.success) { setError(parsed.error.errors[0]?.message || '校验失败') return } setError('') mutation.mutate({ id: openTaskId, data: parsed.data }) }

前后端通用(全栈用法)

  • 前端:表单提交、路由参数、本地存储、第三方 SDK 数据校验。
  • 后端:接口入参校验、配置文件解析、Webhooks 数据保护。
  • tRPC/BFF:用 Zod 定义契约,自动生成前端类型,极简对接。

最佳实践

  • 将 Zod 作为数据边界的“唯一门神”:提交前统一校验与转换。
  • 模式集中管理:一个模块导出 schema 与 z.infer 类型,避免散落各处。
  • 错误语义化:明确错误信息(字段级/汇总),保证用户能修复。
  • 防御性编程:对接口返回也做 Zod 校验(容错与监控)。

常见坑与建议

  • 只写 TS 类型不做运行时校验:会在生产上放过脏数据。
  • 规则分散在各个控件:后续维护困难,建议统一收敛到 Zod。
  • 忘记 trim():表单文本常见边界,建议在 transform/preprocess 中清洗。
  • 复杂规则不落地:跨字段、时间区间、枚举关联用 superRefine

完整示例:任务表单

import { z } from 'zod' import { useMutation } from '@tanstack/react-query' const TaskSchema = z.object({ name: z.string().min(1, '任务名称不能为空').max(30, '任务名称不能超过 30 个字符').transform(s => s.trim()), description: z.string().max(200, '描述不能超过 200 字').optional(), dueDate: z.string().optional(), priority: z.enum(['low', 'medium', 'high']).optional(), }) type TaskInput = z.infer<typeof TaskSchema> const mutation = useMutation({ mutationFn: async ({ id, data }: { id: string; data: TaskInput }) => { // await axios.patch(`/tasks/${id}`, data) await new Promise(res => setTimeout(res, 300)) }, }) function onSubmit(form: unknown, setError: (msg: string) => void) { const parsed = TaskSchema.safeParse(form) if (!parsed.success) { setError(parsed.error.errors[0]?.message ?? '校验失败') return } setError('') mutation.mutate({ id: 't-1', data: parsed.data }) }

总结

  • Zod 是将“类型系统”与“运行时校验”统一起来的基础设施。它让前端/后端的数据边界变得明确且健壮。
  • 在现代前端里,最自然的流程是:控件轻量约束 → Zod 统一校验 → React Query 提交接口 → 成功后状态更新。
  • 一旦习惯用 Zod,你的表单与接口流程会更稳定、更易维护,也更利于类型驱动开发。

如需我将此文档转成项目内的 Markdown 文件并放到指定位置,告诉我路径即可,我会为你生成并保存。

Last updated on