
关于
Zod 专家 — TypeScript 优先的 Schema 验证。涵盖解析、自定义错误、细化、类型推断以及与 React Hook Form、Next.js 和 tRPC 的集成。
name: zod-validation-expert description: "Zod专家——TypeScript优先的模式验证。涵盖解析、自定义错误、细化、类型推断,以及与React Hook Form、Next.js和tRPC的集成。" risk: safe source: community date_added: "2026-03-05"
Zod 验证专家
你是一个生产级Zod专家。你帮助开发者构建类型安全的模式定义和验证逻辑。你精通Zod基础(原语、对象、数组、记录)、类型推断(z.infer)、复杂验证(.refine、.superRefine)、转换(.transform),以及在现代TypeScript生态系统中的集成(React Hook Form、Next.js API Routes / App Router Actions、tRPC和环境变量)。
何时使用此技能
- 为API输入或表单定义TypeScript验证模式时使用
- 设置环境变量验证(
process.env)时使用 - 将Zod与React Hook Form集成(
@hookform/resolvers/zod)时使用 - 从运行时验证模式提取或推断TypeScript类型时使用
- 编写复杂验证规则(如跨字段验证、异步验证)时使用
- 转换输入数据(如字符串转Date、字符串转数字强制转换)时使用
- 标准化错误消息格式时使用
核心概念
为什么选择Zod?
Zod消除了同时编写TypeScript接口和运行时验证模式的重复工作。你只需定义一次模式,Zod就能推断出静态TypeScript类型。注意Zod用于解析,而不仅仅是验证。safeParse 和 parse 返回干净的、类型化的数据,默认剥离未知键。
模式定义与推断
原语与强制转换
import { z } from "zod";
// 基本原语
const stringSchema = z.string().min(3).max(255);
const numberSchema = z.number().int().positive();
const dateSchema = z.date();
// 强制转换(验证前自动转换输入)
// 对Next.js Server Actions中的FormData或URL查询非常有用
const ageSchema = z.coerce.number().min(18); // "18" -> 18
const activeSchema = z.coerce.boolean(); // "true" -> true
const dobSchema = z.coerce.date(); // "2020-01-01" -> Date object
对象与类型推断
const UserSchema = z.object({
id: z.string().uuid(),
username: z.string().min(3).max(20),
email: z.string().email(),
role: z.enum(["ADMIN", "USER", "GUEST"]).default("USER"),
age: z.number().min(18).optional(), // 可以省略
website: z.string().url().nullable(), // 可以为null
tags: z.array(z.string()).min(1), // 至少1项的数组
});
// 直接从模式推断TypeScript类型
// 无需单独编写 `interface User { ... }`
export type User = z.infer<typeof UserSchema>;
高级类型
// 记录(具有动态键但特定值类型的对象)
const envSchema = z.record(z.string(), z.string()); // Record<string, string>
// 联合类型(OR)
const idSchema = z.union([z.string(), z.number()]); // string | number
// 或更简单:
const idSchema2 = z.string().or(z.number());
// 可辨识联合(类型安全的switch case)
const ActionSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal("create"), id: z.string() }),
z.object({ type: z.literal("update"), id: z.string(), data: z.any() }),
z.object({ type: z.literal("delete"), id: z.string() }),
]);
解析与验证
parse vs safeParse
const schema = z.string().email();
// ❌ parse:验证失败时抛出ZodError
try {
const email = schema.parse("invalid-email");
} catch (err) {
if (err instanceof z.ZodError) {
console.error(err.issues);
}
}
// ✅ safeParse:返回结果对象(无需try/catch)
const result = schema.safeParse("user@example.com");
if (!result.success) {
// TypeScript将result收窄为SafeParseError
console.log(result.error.format());
// 提前返回或抛出领域错误
} else {
// TypeScript将result收窄为SafeParseSuccess
const validEmail = result.data; // 类型为 `string`
}
自定义验证
自定义错误消息
const passwordSchema = z.string()
.min(8, { message: "密码必须至少8个字符" })
.max(100, { message: "密码太长" })
.regex(/[A-Z]/, { message: "密码必须包含至少一个大写字母" })
.regex(/[0-9]/, { message: "密码必须包含至少一个数字" });
// 全局自定义错误映射(用于国际化)
z.setErrorMap((issue, ctx) => {
if (issue.code === z.ZodIssueCode.invalid_type) {
if (issue.expected === "string") return { message: "此字段必须是文本" };
}
return { message: ctx.defaultError };
});
细化(自定义逻辑)
// 基本细化
const passwordCheck = z.string().refine((val) => val !== "password123", {
message: "密码太弱",
});
// 跨字段验证(如密码匹配)
const formSchema = z.object({
password: z.string().min(8),
confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
message: "密码不匹配",
path: ["confirmPassword"], // 错误附加到此字段
});
superRefine(多错误)
const complexSchema = z.string().superRefine((val, ctx) => {
if (val.length < 8) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "至少8个字符",
});
}
if (!/[A-Z]/.test(val)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "需要大写字母",
});
}
});
转换
// 解析后转换数据
const trimmedString = z.string().transform((val) => val.trim());
const upperCase = z.string().transform((val) => val.toUpperCase());
// 链式验证 + 转换
const ageFromString = z.string()
.transform((val) => parseInt(val, 10))
.pipe(z.number().min(0).max(150));
React Hook Form 集成
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
const loginSchema = z.object({
email: z.string().email("请输入有效的邮箱地址"),
password: z.string().min(8, "密码至少8个字符"),
});
type LoginForm = z.infer<typeof loginSchema>;
function LoginPage() {
const { register, handleSubmit, formState: { errors } } = useForm<LoginForm>({
resolver: zodResolver(loginSchema),
});
const onSubmit = (data: LoginForm) => {
// data已完全类型化和验证
console.log(data.email, data.password);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("email")} />
{errors.email && <span>{errors.email.message}</span>}
<input type="password" {...register("password")} />
{errors.password && <span>{errors.password.message}</span>}
<button type="submit">登录</button>
</form>
);
}
环境变量验证
const envSchema = z.object({
DATABASE_URL: z.string().url(),
API_KEY: z.string().min(1),
PORT: z.coerce.number().default(3000),
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
});
export const env = envSchema.parse(process.env);
最佳实践
- 优先使用
safeParse而非parse以获得更好的错误处理 - 使用
z.infer从模式派生类型,避免重复 - 对FormData和URL参数使用
z.coerce - 在应用启动时验证环境变量
- 为可重用验证逻辑使用
.refine和.superRefine
兼容工具
Claude CodeCursor
标签
前端开发