Zod 校验

20 阅读7分钟

Zod 校验

Zod 是一个 TypeScript 优先的模式验证库,核心优势是类型安全运行时校验的结合,既能在编译期提供类型提示,也能在运行期校验数据(如 API 响应、表单输入)。

一、基础用法(入门级)

首先确保你的项目已安装 Zod:

  1. 安装 Zod
npm install zod
# 或 yarn/pnpm
yarn add zod
pnpm add zod
  1. 基本类型校验

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); // 校验通过的有效数据
}
  1. 基础类型的约束

给基础类型添加额外规则(如字符串长度、数字范围):

// 字符串约束
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. 复合类型:对象、数组、元组

(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]
  1. 联合类型与字面量类型
// 字面量类型:固定值
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"); // 合法
  1. 部分 / 可选 / 必填转换

对已定义的对象模式快速修改字段的可选 / 必填状态:

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 });

三、高级用法(进阶场景)

  1. 自定义校验(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 → 报错
  1. 递归模式(处理嵌套 / 循环结构) 适用于树形结构(如分类、评论回复):
// 定义递归模式(先声明,后赋值)
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); // 合法
  1. 拦截器(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
  1. 错误处理与自定义错误格式 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: "年龄必须是整数" }
  // ]
}
  1. 与 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']
});

总结

  1. 基础核心:Zod 以 z.类型() 定义模式,通过 .parse()/.safeParse() 执行校验,z.infer 提取 TypeScript 类型是核心优势。
  2. 中级重点:对象(z.object)、数组(z.array)、联合类型(z.union/z.enum)是日常开发最常用的复合类型,partial()/required() 可灵活修改字段可选性。
  3. 高级技巧:refine 实现自定义校验、transform 处理类型转换、z.lazy 支持递归结构、preprocess 预处理数据,结合错误格式化可适配表单 / API 校验场景。