Zod 常用案例总结
官方文档:https://zod.dev/
一、先懂核心(零基础必看)
1. Zod 是什么?
Zod 是 TS 优先的运行时数据校验库,核心解决两个问题:
- 校验运行时数据(如表单、接口返回)是否符合预期;
- 从校验规则自动推导 TS 类型,无需手动写 interface。
2. 核心概念(3个关键词)
| 概念 | 说明 |
|---|---|
| Schema | 校验规则(比如 z.string() 是字符串校验规则) |
| safeParse | 安全校验方法(失败不抛异常,返回结果对象) |
| z.infer | 从 Schema 自动推导 TS 类型的工具函数 |
3. 前置准备(1步搞定)
bash
# 安装 Zod(仅需这1行命令)
npm install zod
二、基础入门(从零开始写)
场景1:单个基础类型校验(新手第一步)
目标
学会定义最简单的校验规则,用 safeParse 校验数据
代码(可直接复制)
typescript
import { z } from "zod";
// 1. 定义 Schema(校验规则)
const strSchema = z.string(); // 校验字符串
const numSchema = z.number(); // 校验数字
const boolSchema = z.boolean(); // 校验布尔值
// 2. 用 safeParse 校验数据(核心方法)
// 校验成功
const successResult = strSchema.safeParse("hello zod");
console.log(successResult.success); // true
console.log(successResult.data); // "hello zod"
// 校验失败
const failResult = strSchema.safeParse(123); // 传入数字,校验失败
console.log(failResult.success); // false
console.log(failResult.error.issues[0].message); // "Expected string, received number"
场景2:给基础类型加规则(常用修饰符)
目标
学会给规则加限制(如长度、范围),自定义错误提示
代码(可直接复制)
typescript
import { z } from "zod";
// 字符串规则:长度限制 + 自定义提示 + 自动去空格
const usernameSchema = z.string()
.min(3, "用户名至少3个字符") // 最小长度 + 自定义错误
.max(20, "用户名最多20个字符") // 最大长度
.trim(); // 自动去除首尾空格
// 数字规则:整数 + 范围限制
const ageSchema = z.number()
.int("年龄必须是整数") // 仅允许整数
.min(18, "年龄不能小于18") // 最小值
.max(120, "年龄不能大于120"); // 最大值
// 可选字段 + 默认值(高频)
const rememberSchema = z.boolean()
.optional() // 允许字段不存在/为 undefined
.default(false); // 无值时自动填充默认值
// 测试默认值
console.log(rememberSchema.safeParse(undefined).data); // false
三、核心实战(最常用场景,覆盖90%需求)
场景3:对象校验(表单/接口核心)
目标
校验对象(如表单数据、接口返回),自动推导 TS 类型
代码(可直接复制)
typescript
import { z } from "zod";
// 1. 定义对象 Schema(支持嵌套)
const UserSchema = z.object({
name: z.string().min(2),
age: z.number().int().min(18),
email: z.string().email("邮箱格式错误").optional(), // 可选字段
address: z.object({ // 嵌套对象
city: z.string().nonempty("城市不能为空")
})
});
// 2. 自动推导 TS 类型(Zod 核心价值,无需手写 interface)
type User = z.infer<typeof UserSchema>;
// 3. 通用校验函数(前端表单/接口通用)
const validateUser = (data: unknown) => {
const result = UserSchema.safeParse(data);
if (result.success) {
return { valid: true, data: result.data as User };
}
// 格式化错误:适配前端表单({ 字段名: 错误提示 })
const errors = result.error.issues.reduce((acc, issue) => {
acc[issue.path.join(".")] = issue.message; // 嵌套字段如 "address.city"
return acc;
}, {} as Record<string, string>);
return { valid: false, errors };
};
// 测试
const testData = {
name: "张三",
age: 20,
address: { city: "" } // 城市为空,校验失败
};
console.log(validateUser(testData));
// 输出:{ valid: false, errors: { "address.city": "城市不能为空" } }
场景4:自定义规则(两次密码一致)
目标
解决内置规则满足不了的业务场景(如表单密码确认)
代码(可直接复制)
typescript
import { z } from "zod";
// 定义规则 + 自定义校验
const PasswordSchema = z.object({
password: z.string().min(6, "密码至少6位"),
confirmPassword: z.string()
}).refine(
(data) => data.password === data.confirmPassword, // 校验逻辑
{
message: "两次密码不一致",
path: ["confirmPassword"] // 错误绑定到 confirmPassword 字段
}
);
// 测试
const result = PasswordSchema.safeParse({
password: "123456",
confirmPassword: "654321"
});
console.log(result.error.issues[0].message); // "两次密码不一致"
场景5:数组/联合类型(补充高频)
目标
校验数组(如爱好列表)、多类型值(如 ID 支持字符串/数字)
代码(可直接复制)
typescript
import { z } from "zod";
// 1. 数组校验:字符串数组,最少1个元素
const hobbiesSchema = z.array(z.string()).min(1, "至少选1个爱好");
console.log(hobbiesSchema.safeParse([]).error.issues[0].message); // "至少选1个爱好"
// 2. 联合类型:ID 支持 UUID 字符串 或 数字
const idSchema = z.union([z.string().uuid(), z.number().int()]);
console.log(idSchema.safeParse(123).success); // true
console.log(idSchema.safeParse("abc").success); // false
场景6:React + TS 表单完整实战
目标
整合所有用法,实现可直接运行的 React 表单校验
代码(可直接复制运行)
javascript
import { useState } from "react";
import { z } from "zod";
// 1. 定义表单 Schema
const LoginSchema = z.object({
username: z.string().min(3, "用户名至少3位"),
password: z.string().min(6, "密码至少6位"),
remember: z.boolean().optional().default(false)
});
// 自动推导表单类型
type LoginForm = z.infer<typeof LoginSchema>;
// 2. 封装校验函数
const validateLogin = (data: unknown) => {
const result = LoginSchema.safeParse(data);
if (result.success) return { valid: true, data: result.data };
const errors = result.error.issues.reduce((acc, i) => ({ ...acc, [i.path[0]]: i.message }), {});
return { valid: false, errors };
};
export default function LoginForm() {
// 表单状态 + 错误状态
const [form, setForm] = useState<LoginForm>({
username: "",
password: "",
remember: false
});
const [errors, setErrors] = useState<Record<string, string>>({});
// 输入变更处理
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value, type, checked } = e.target;
setForm(prev => ({
...prev,
[name]: type === "checkbox" ? checked : value
}));
// 输入时清除对应错误
if (errors[name]) setErrors(prev => ({ ...prev, [name]: "" }));
};
// 提交校验
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const { valid, errors: newErrors } = validateLogin(form);
if (!valid) {
setErrors(newErrors);
return;
}
// 校验通过,调用接口
console.log("提交数据:", form);
};
// 渲染表单
return (
<form onSubmit={handleSubmit} style={{ padding: 20 }}>
<div style={{ marginBottom: 10 }}>
<label>用户名:</label>
<input
name="username"
value={form.username}
onChange={handleChange}
/>
{errors.username && <span style={{ color: "red" }}>{errors.username}</span>}
</div>
<div style={{ marginBottom: 10 }}>
<label>密码:</label>
<input
type="password"
name="password"
value={form.password}
onChange={handleChange}
/>
{errors.password && <span style={{ color: "red" }}>{errors.password}</span>}
</div>
<div style={{ marginBottom: 10 }}>
<input
type="checkbox"
name="remember"
checked={form.remember}
onChange={handleChange}
/>
<label>记住我</label>
</div>
<button type="submit">登录</button>
</form>
);
}
四、新手避坑3条(必看)
- 优先用 safeParse 而非 parse:parse 校验失败会抛异常,需要 try/catch;safeParse 返回结果对象,前端更友好;
- 类型推导只写一行 :
type 类型名 = z.infer<typeof Schema名>,无需手动定义 interface; - 错误格式化固定写法 :
issue.path.join(".")处理嵌套字段,适配前端表单字段名。