表单验证是前端开发中无法回避的一环。传统做法通常是在提交时手动检查字段、编写大量 if-else,或者依赖某个 UI 库自带的校验规则。这些方案能跑通,但有一个共同的痛点:类型定义和运行时校验是两套东西。你在 TypeScript 里声明了一个 User 接口,又在表单验证逻辑里重新写一遍字段规则,两边一旦不同步,bug 就来了。
Zod 的出现让这个问题有了优雅的解法。它是一个以 TypeScript 为中心的 schema 声明与验证库,核心思路是:schema 即类型,类型即 schema。你只需要定义一次,TypeScript 类型和运行时验证逻辑就同时有了。
为什么是 Zod
Zod 的设计有几个关键优势:
- 零依赖,体积小,tree-shaking 友好
- 类型推断:通过
z.infer<typeof schema>直接从 schema 推导出 TypeScript 类型,不需要手写 interface - 链式 API:
z.string().min(3).email()这样的写法直观且可组合 - 错误信息结构化:验证失败时返回的
ZodError包含每个字段的详细错误路径和消息,方便映射到表单 UI
与 Yup、Joi 等老牌方案相比,Zod 最大的不同是它对 TypeScript 类型系统的深度利用。Yup 虽然也支持类型推断,但在复杂联合类型、 discriminated union 等场景下,Zod 的表现更自然。
从 Schema 到类型:一次定义,两端使用
假设我们要做一个用户注册表单,包含用户名、邮箱、密码和确认密码。用 Zod 定义 schema:
import { z } from "zod";
const registerSchema = z
.object({
username: z
.string()
.min(3, "用户名至少 3 个字符")
.max(20, "用户名最多 20 个字符"),
email: z.string().email("请输入有效的邮箱地址"),
password: z
.string()
.min(8, "密码至少 8 位")
.regex(/[A-Z]/, "密码需包含大写字母")
.regex(/[0-9]/, "密码需包含数字"),
confirmPassword: z.string(),
})
.refine((data) => data.password === data.confirmPassword, {
message: "两次输入的密码不一致",
path: ["confirmPassword"],
});
type RegisterForm = z.infer<typeof registerSchema>;
注意最后一行。RegisterForm 这个类型完全从 schema 推导而来,包含 username、email、password、confirmPassword 四个 string 字段。如果你后续修改了 schema,比如给 username 加上 .optional(),RegisterForm 会自动变成可选,所有使用该类型的地方都会得到正确的类型提示。
这就是“端到端类型安全”的起点:类型不再是你手动维护的契约,而是 schema 的副产品。
在表单中集成 Zod
以 React Hook Form 为例,它和 Zod 的集成非常顺滑。通过 @hookform/resolvers/zod,你可以直接把 Zod schema 作为 resolver 传入:
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
function RegisterForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<RegisterForm>({
resolver: zodResolver(registerSchema),
});
const onSubmit = (data: RegisterForm) => {
// data 已经通过 Zod 验证,类型完全安全
console.log(data.email); // string
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("username")} />
{errors.username && <span>{errors.username.message}</span>}
<input {...register("email")} />
{errors.email && <span>{errors.email.message}</span>}
<input type="password" {...register("password")} />
{errors.password && <span>{errors.password.message}</span>}
<input type="password" {...register("confirmPassword")} />
{errors.confirmPassword && <span>{errors.confirmPassword.message}</span>}
<button type="submit">注册</button>
</form>
);
}
这里有几个关键点:
useForm<RegisterForm>的泛型来自z.infer,表单字段名和类型都有自动补全zodResolver在提交时执行验证,错误信息自动填充到errors对象onSubmit收到的data已经过验证,不需要再做任何运行时检查
如果你不用 React Hook Form,也可以手动调用 registerSchema.safeParse(formData),根据返回的 success 字段决定后续逻辑。safeParse 不会抛异常,而是返回一个 discriminated union:
const result = registerSchema.safeParse(formData);
if (!result.success) {
// result.error.issues 包含所有验证错误
const fieldErrors = result.error.flatten().fieldErrors;
// fieldErrors.email 是一个 string[] | undefined
} else {
// result.data 类型为 RegisterForm
}
flatten() 方法把错误按字段分组,非常适合直接映射到表单 UI。
服务端复用同一套 Schema
端到端类型安全的关键在于“端到端”。Zod schema 不依赖任何浏览器 API,可以无缝地在 Node.js 服务端复用。假设你用 Express 或 Next.js API Route 处理注册请求:
import { registerSchema } from "./schemas";
app.post("/api/register", async (req, res) => {
const result = registerSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
errors: result.error.flatten().fieldErrors,
});
}
const { username, email, password } = result.data;
// 这里 password 已经满足强度要求,email 格式已验证
// 直接进入业务逻辑
});
前端和后端共享同一个 schema 文件,意味着:
- 前端提示的验证规则和后端强制执行的规则完全一致
- 修改验证逻辑时只需改一处
- API 的请求体类型和响应类型都可以从 schema 推导,前后端接口契约不会漂移
如果你使用 tRPC 或类似的端到端类型安全框架,Zod 更是天然的一等公民。tRPC 的 input 验证直接接受 Zod schema,客户端调用时自动获得类型提示和运行时校验。
进阶模式: discriminated union 与条件验证
实际业务中,表单往往不是扁平的。比如一个支付表单,根据支付方式不同,需要填写的字段也不同:
const paymentSchema = z.discriminatedUnion("method", [
z.object({
method: z.literal("card"),
cardNumber: z.string().length(16),
expiry: z.string(),
cvv: z.string().length(3),
}),
z.object({
method: z.literal("bank"),
bankAccount: z.string(),
routingNumber: z.string(),
}),
]);
z.discriminatedUnion 会根据 method 字段的值自动选择对应的分支进行验证。推导出的类型也是一个联合类型,在代码中通过 if (data.method === "card") 就能收窄到正确的分支,TypeScript 会自动提示 cardNumber 等字段。
这种模式在传统表单验证库中很难优雅实现,而 Zod 借助 TypeScript 的 discriminated union 类型,做到了类型和验证逻辑的完全同步。
总结
Zod 解决的核心问题是 schema 和类型的单一来源。在它之前,你要么手写两套(类型 + 验证),要么接受验证库类型推断能力的不足。Zod 让“定义一次,两端使用”成为现实:
- 前端表单验证用
zodResolver,错误信息结构化 - 后端 API 验证用
safeParse,请求体类型自动推导 - 复杂场景用
discriminatedUnion、refine、transform等组合子,类型收窄依然准确
如果你的项目已经用 TypeScript,但表单验证还在手写 if-else 或者类型和校验逻辑各写各的,Zod 值得一试。它不会让你的代码变复杂,反而会减少你需要维护的代码量——尤其是那些你写了两遍、却总有一遍忘了更新的字段规则。
未经允许不得转载:任鹏个人博客 » TypeScript 与 Zod 结合:构建端到端类型安全的表单验证方案


朋友圈点赞图在线生成源码