Zod 校验
Zod 是一个 TypeScript 优先的模式验证库,核心优势是类型安全和运行时校验的结合,既能在编译期提供类型提示,也能在运行期校验数据(如 API 响应、表单输入)。
一、基础用法(入门级)
首先确保你的项目已安装 Zod:
- 安装 Zod
npm install zod
# 或 yarn/pnpm
yarn add zod
pnpm add zod
- 基本类型校验
Zod 支持所有基础类型的校验,核心语法是 z.类型() 定义模式,.parse() 执行校验(校验失败会抛出错误),.safeParse() 安全校验(返回结果对象)。
import { z } from "zod";
// 1. 基础类型定义
const StringSchema = z.string(); // 字符串类型
const NumberSchema = z.number(); // 数字类型
const BooleanSchema = z.boolean(); // 布尔类型
const NullSchema = z.null(); // null 类型
const UndefinedSchema = z.undefined(); // undefined 类型
const AnySchema = z.any(); // 任意类型(不推荐,失去类型安全)
const UnknownSchema = z.unknown(); // 未知类型(推荐用于未知数据源)
// 2. 执行校验
// 成功示例
const validString = StringSchema.parse("hello zod"); // 输出: "hello zod"
// 失败示例(抛出 ZodError)
// const invalidString = StringSchema.parse(123);
// 安全校验(推荐,避免抛出错误)
const result = StringSchema.safeParse(123);
if (!result.success) {
console.log(result.error); // 打印详细的错误信息
} else {
console.log(result.data); // 校验通过的有效数据
}
- 基础类型的约束
给基础类型添加额外规则(如字符串长度、数字范围):
// 字符串约束
const UsernameSchema = z.string()
.min(3, "用户名至少3个字符") // 最小长度 + 自定义错误信息
.max(20, "用户名最多20个字符")
.email("请输入合法邮箱"); // 邮箱格式校验
// 数字约束
const AgeSchema = z.number()
.int("年龄必须是整数") // 整数
.positive("年龄必须为正数")
.min(18, "必须年满18岁")
.max(120, "年龄超出合理范围");
// 布尔值的特殊约束(非空)
const AgreeSchema = z.boolean().refine(val => val === true, {
message: "必须同意协议"
});
二、中级用法(常用场景)
- 复合类型:对象、数组、元组
(1)对象校验(最常用)
// 定义用户对象模式
const UserSchema = z.object({
name: z.string().min(2),
age: z.number().int().positive(),
email: z.string().email().optional(), // 可选字段
isVip: z.boolean().default(false), // 默认值(校验时自动填充)
hobbies: z.array(z.string()), // 字符串数组
});
// 校验数据
const userData = {
name: "张三",
age: 25,
hobbies: ["篮球", "编程"]
};
const validatedUser = UserSchema.parse(userData);
console.log(validatedUser.isVip); // 输出: false(默认值生效)
// 提取 TypeScript 类型(核心优势)
type User = z.infer<typeof UserSchema>;
// type User = {
// name: string;
// age: number;
// email?: string | undefined;
// isVip: boolean;
// hobbies: string[];
// }
(2)数组与元组
// 数组:元素类型统一
const NumberArraySchema = z.array(z.number()).nonempty("数组不能为空"); // 非空数组
// 元组:固定长度 + 固定类型(如 [string, number])
const PointSchema = z.tuple([z.number(), z.number()]); // 二维坐标 [x, y]
const point = PointSchema.parse([10, 20]); // 合法
// PointSchema.parse([10]); // 报错:长度不足
// 提取元组类型
type Point = z.infer<typeof PointSchema>; // type Point = [number, number]
- 联合类型与字面量类型
// 字面量类型:固定值
const GenderSchema = z.enum(["male", "female", "other"]); // 枚举(推荐)
// 或 z.literal("male").or(z.literal("female")).or(z.literal("other"))
// 联合类型:多种类型/值可选
const IdSchema = z.string().or(z.number()); // ID 可以是字符串或数字
const StatusSchema = z.union([z.literal("success"), z.literal("error"), z.literal("pending")]);
// 校验示例
GenderSchema.parse("male"); // 合法
IdSchema.parse(123); // 合法
IdSchema.parse("abc123"); // 合法
StatusSchema.parse("success"); // 合法
- 部分 / 可选 / 必填转换
对已定义的对象模式快速修改字段的可选 / 必填状态:
const BaseUserSchema = z.object({
name: z.string(),
age: z.number(),
email: z.string()
});
// 所有字段可选
const PartialUserSchema = BaseUserSchema.partial();
// 仅指定字段可选
const PartialEmailSchema = BaseUserSchema.partial({ email: true });
// 所有字段必填(抵消 partial)
const RequiredUserSchema = PartialUserSchema.required();
// 仅指定字段必填
const RequiredNameSchema = PartialUserSchema.required({ name: true });
三、高级用法(进阶场景)
- 自定义校验(refine/transform) (1)refine:自定义校验逻辑
// 密码校验:至少包含字母和数字
const PasswordSchema = z.string()
.min(8)
.refine(
(password) => /^(?=.*[a-zA-Z])(?=.*\d)/.test(password),
{
message: "密码必须包含字母和数字",
path: ["password"], // 指定错误路径(方便定位)
}
);
// 多字段联动校验(对象级别)
const RegisterSchema = z.object({
password: z.string().min(8),
confirmPassword: z.string()
}).refine(data => data.password === data.confirmPassword, {
message: "两次密码不一致",
path: ["confirmPassword"], // 错误指向确认密码字段
});
(2)transform:类型转换 + 校验
// 示例:将字符串数字转为数字类型,再校验范围
const StringToNumberSchema = z.string()
.transform((val) => parseInt(val, 10)) // 转换为数字
.refine((val) => !isNaN(val), { message: "必须是有效数字" }) // 校验转换结果
.refine((val) => val > 0, { message: "必须是正数" });
// 校验 "123" → 转换为 123 → 校验通过
console.log(StringToNumberSchema.parse("123")); // 输出: 123
// 校验 "abc" → 转换为 NaN → 报错
- 递归模式(处理嵌套 / 循环结构) 适用于树形结构(如分类、评论回复):
// 定义递归模式(先声明,后赋值)
const CategorySchema: z.ZodSchema<{ id: number; name: string; children?: typeof CategorySchema[] }> = z.lazy(() =>
z.object({
id: z.number(),
name: z.string(),
children: z.array(CategorySchema).optional() // 子分类(递归)
})
);
// 校验树形数据
const categoryData = {
id: 1,
name: "电子产品",
children: [
{ id: 2, name: "手机" },
{ id: 3, name: "电脑", children: [{ id: 4, name: "笔记本" }] }
]
};
CategorySchema.parse(categoryData); // 合法
- 拦截器(preprocess):预处理数据 在正式校验前修改数据(如去除空格、转换空值):
// 示例:去除字符串前后空格,空字符串转为 undefined
const TrimmedStringSchema = z.preprocess(
(val) => {
if (typeof val === "string") {
const trimmed = val.trim();
return trimmed === "" ? undefined : trimmed;
}
return val;
},
z.string().optional() // 最终校验规则
);
// 校验示例
console.log(TrimmedStringSchema.parse(" hello ")); // 输出: "hello"
console.log(TrimmedStringSchema.parse(" ")); // 输出: undefined
- 错误处理与自定义错误格式 Zod 内置的 ZodError 包含详细的错误信息,可格式化输出:
const UserSchema = z.object({
name: z.string().min(2, "姓名至少2个字符"),
age: z.number().int("年龄必须是整数").min(18, "年龄需≥18")
});
const result = UserSchema.safeParse({ name: "李", age: 17.5 });
if (!result.success) {
// 格式化错误信息(提取字段 + 错误提示)
const errors = result.error.issues.map(issue => ({
field: issue.path.join("."), // 字段路径(嵌套对象用 . 分隔)
message: issue.message
}));
console.log(errors);
// 输出:
// [
// { field: "name", message: "姓名至少2个字符" },
// { field: "age", message: "年龄必须是整数" }
// ]
}
- 与 TypeScript 深度结合
利用
z.infer提取类型,结合泛型实现类型安全:
// 封装通用的 API 响应校验函数
const ApiResponseSchema = <T extends z.ZodSchema>(dataSchema: T) =>
z.object({
code: z.number().int().default(200),
msg: z.string().default("success"),
data: dataSchema
});
// 定义用户列表响应模式
const UserListResponseSchema = ApiResponseSchema(z.array(UserSchema));
// 提取响应类型
type UserListResponse = z.infer<typeof UserListResponseSchema>;
// type UserListResponse = {
// code: number;
// msg: string;
// data: User[];
// }
// 校验 API 响应
const mockApiResponse = {
code: 200,
msg: "success",
data: [{ name: "张三", age: 25, hobbies: ["篮球"] }]
};
const validatedResponse = UserListResponseSchema.parse(mockApiResponse);
日期、单选按钮组校验
// 工具函数:验证日期有效性(兼容 undefined/空值)
const isValidDate = (date: Date | string | undefined | null) => {
if (!date) return false; // 空值直接返回false
const parsedDate = typeof date === 'string' ? parseISO(date) : date;
return isValid(parsedDate);
};
// 工具函数:日期格式化(统一处理 Date 对象和字符串)
const formatDate = (date: Date | string | undefined, formatStr = 'yyyy-MM-dd HH:mm:ss') => {
if (!date) return '';
const parsedDate = typeof date === 'string' ? parseISO(date) : date;
return isValid(parsedDate) ? format(parsedDate, formatStr) : '';
};
// 表单验证规则(提交时验证)
const authFormSchema = z.object({
phone: z.string().min(11, '手机号必须是11位').max(11, '手机号必须是11位'),
smsCode: z.string().min(4, '验证码必须是4位').max(6, '验证码必须是6位'),
authType: z.string()
.refine(val => val !== '', { message: '请选择授权类型' }) // 第一层:非空
.refine(val => ['1', '2'].includes(val), { message: '请选择有效的授权类型' }) // 第二层:合法值
.default(''), // 避免初始值 undefined 触发错误
authStartTime: z.unknown() // 最宽松的类型,避开内置类型校验
.refine(val => val !== undefined && val !== '', {
message: '请选择授权开始日期' // 空值提示(中文)
})
.refine(val => isValidDate(val as Date | string), {
message: '请选择有效的授权开始日期' // 无效日期提示(中文)
})
.refine(val => {
if (!val) return true; // 空值已由上一层校验,这里跳过
const currentDate = new Date();
currentDate.setHours(0, 0, 0, 0);
const parsedDate = typeof val === 'string' ? parseISO(val) : (val as Date);
parsedDate.setHours(0, 0, 0, 0);
return !isBefore(parsedDate, currentDate);
}, { message: '授权开始日期不能早于今天' }),
authEndTime: z.unknown() // 同样用 unknown 避开内置校验
.refine(val => val !== undefined && val !== '', {
message: '请选择授权结束日期'
})
.refine(val => isValidDate(val as Date | string), {
message: '请选择有效的授权结束日期'
}),
isPermanent: z.boolean().default(false),
authNumber: z.number().min(1, '授权次数至少为1次').default(1),
remark: z.string().optional(),
loginPassword: z.string().min(6, '密码至少6位'),
confirmLoginPassword: z.string().min(6, '确认密码至少6位'),
}).refine((data) => data.loginPassword === data.confirmLoginPassword, {
message: "两次密码不匹配",
path: ["confirmLoginPassword"],
}).refine((data) => {
// 校验结束日期 >= 开始日期
if (!data.authStartTime || !data.authEndTime) return true; // 已有单独的必填校验
const start = typeof data.authStartTime === 'string' ? parseISO(data.authStartTime) : data.authStartTime;
const end = typeof data.authEndTime === 'string' ? parseISO(data.authEndTime) : data.authEndTime;
return !isBefore(end, start);
}, {
message: '授权结束日期不能早于开始日期',
path: ['authEndTime']
});
总结:
- 基础核心:Zod 以
z.类型()定义模式,通过.parse()/.safeParse()执行校验,z.infer提取 TypeScript 类型是核心优势。 - 中级重点:对象(
z.object)、数组(z.array)、联合类型(z.union/z.enum)是日常开发最常用的复合类型,partial()/required()可灵活修改字段可选性。 - 高级技巧:
refine实现自定义校验、transform处理类型转换、z.lazy支持递归结构、preprocess预处理数据,结合错误格式化可适配表单 / API 校验场景。