zod教程
一、简介
Zod 是一个 TypeScript 首选的模式声明和验证库,具有以下特点:
- 零依赖
- 体积小(8kb)
- 与 TypeScript 完美集成
- 链式 API
- 支持同步和异步验证
二、安装
npm install zod
yarn add zod
pnpm add zod
三、基础数据类型验证
3.1 原始类型
z.string()
z.number()
z.boolean()
z.bigint()
z.symbol()
z.null()
z.undefined()
z.void()
z.any()
z.unknown()
z.never()
z.date()
3.2 可选和可为空
z.string().optional()
z.string().optional().null()
z.string().nullable()
z.string().default("默认值")
四、对象验证
4.1 基础对象验证
const userSchema = z.object({
username: z.string().min(3).max(20),
email: z.string().email(),
age: z.number().int().positive().optional(),
isAdmin: z.boolean().default(false),
});
4.2 验证方法
const user = userSchema.parse(data);
const result = userSchema.safeParse(data);
if (result.success) {
console.log(result.data);
} else {
console.log(result.error.errors);
}
userSchema.partial().parse(data);
userSchema.deepPartial().parse(data);
userSchema.strict().parse(data);
userSchema.strip().parse(data);
userSchema.passthrough().parse(data);
五、数组和元组
5.1 数组
z.array(z.string())
z.string().array()
z.array(z.string())
.min(3, "至少需要3个元素")
.max(10, "最多10个元素")
.length(5, "必须正好5个元素")
z.string().array().nonempty()
5.2 元组
z.tuple([
z.string(),
z.number(),
z.boolean().optional()
]);
六、字面量、枚举和联合类型
6.1 字面量
z.literal("hello")
z.literal(42)
z.literal(true)
6.2 枚举
const statusEnum = z.enum(["pending", "active", "inactive"]);
enum UserRole {
ADMIN = "admin",
USER = "user",
GUEST = "guest"
}
const roleSchema = z.nativeEnum(UserRole);
6.3 联合类型
const idSchema = z.union([
z.string().uuid(),
z.number().int().positive(),
]);
const eventSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal("click"),
x: z.number(),
y: z.number(),
}),
z.object({
type: z.literal("keypress"),
key: z.string(),
ctrlKey: z.boolean().optional(),
}),
]);
七、字符串和数字验证
7.1 字符串验证方法
7.2 数字验证方法
八、转换和预处理
8.1 类型转换
z.coerce.string()
z.coerce.number()
z.coerce.boolean()
z.coerce.bigint()
z.coerce.date()
z.coerce.number().parse("123");
z.coerce.boolean().parse("true");
8.2 预处理
const trimSchema = z.preprocess(
(val) => (typeof val === "string" ? val.trim() : val),
z.string()
);
const dateStringSchema = z.string()
.refine((val) => !isNaN(Date.parse(val)), {
message: "无效的日期格式"
})
.transform((val) => new Date(val));
九、自定义验证
9.1 使用 refine()
const evenSchema = z.number()
.refine((n) => n % 2 === 0, {
message: "必须是偶数",
});
const uniqueEmailSchema = z.string()
.email()
.refine(async (email) => {
const exists = await checkEmailExists(email);
return !exists;
}, {
message: "邮箱已被注册",
});
const passwordSchema = z.object({
password: z.string().min(6),
confirmPassword: z.string().min(6),
})
.refine((data) => data.password === data.confirmPassword, {
message: "两次输入的密码不一致",
path: ["confirmPassword"],
});
9.2 使用 superRefine()
const userSchema = z.object({
username: z.string(),
email: z.string(),
})
.superRefine((data, ctx) => {
if (data.username === data.email) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "用户名和邮箱不能相同",
path: ["email"],
});
}
});
十、错误处理
10.1 错误结构
try {
schema.parse(data);
} catch (error) {
if (error instanceof z.ZodError) {
error.errors.forEach((err) => {
console.log({
path: err.path.join('.'),
message: err.message,
code: err.code,
});
});
}
}
10.2 错误代码
10.3 自定义错误信息
const schema = z.object({
email: z.string({
required_error: "邮箱是必填项",
invalid_type_error: "邮箱必须是字符串",
}).email({
message: "请输入有效的邮箱地址",
}).min(5, {
message: "邮箱至少需要5个字符",
}),
});
十一、类型推断
11.1 基础类型推断
const userSchema = z.object({
name: z.string(),
age: z.number(),
email: z.string().email().optional(),
});
type User = z.infer<typeof userSchema>;
11.2 实用类型操作
type PartialUser = z.infer<typeof userSchema.partial()>;
type RequiredUser = z.infer<typeof userSchema.required()>;
type NameOnly = z.infer<typeof userSchema.pick({ name: true })>;
type WithoutAge = z.infer<typeof userSchema.omit({ age: true })>;
十二、模式操作
12.1 模式组合
const baseSchema = z.object({
id: z.string().uuid(),
createdAt: z.date(),
});
const userSchema = baseSchema.extend({
name: z.string(),
email: z.string().email(),
});
const schemaA = z.object({ a: z.string() });
const schemaB = z.object({ b: z.number() });
const mergedSchema = schemaA.merge(schemaB);
const personSchema = z.object({ name: z.string() });
const employeeSchema = z.object({ id: z.number() });
const personEmployeeSchema = personSchema.and(employeeSchema);
12.2 递归模式
const categorySchema: z.ZodType<Category> = z.lazy(() =>
z.object({
name: z.string(),
subcategories: z.array(categorySchema).optional(),
})
);
const createCategorySchema = (): z.ZodType<Category> =>
z.object({
name: z.string(),
subcategories: z.array(createCategorySchema()).optional(),
});
十三、实用示例
13.1 表单验证
const loginSchema = z.object({
email: z.string().email("请输入有效的邮箱"),
password: z.string()
.min(6, "密码至少6位")
.max(20, "密码最多20位")
.regex(/[A-Za-z]/, "必须包含字母")
.regex(/\d/, "必须包含数字"),
rememberMe: z.boolean().optional(),
captcha: z.string().length(4, "验证码必须是4位"),
});
const registerSchema = z.object({
username: z.string()
.min(3, "用户名至少3位")
.max(20, "用户名最多20位")
.regex(/^[a-zA-Z0-9_]+$/, "只能包含字母、数字和下划线"),
email: z.string().email(),
password: z.string().min(6),
confirmPassword: z.string(),
acceptTerms: z.boolean().refine((val) => val === true, {
message: "必须接受条款",
}),
}).refine((data) => data.password === data.confirmPassword, {
message: "密码不匹配",
path: ["confirmPassword"],
});
13.2 API 验证
const paginationSchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
sort: z.string().optional(),
order: z.enum(["asc", "desc"]).default("desc"),
search: z.string().optional(),
});
const apiResponseSchema = <T extends z.ZodTypeAny>(dataSchema: T) =>
z.object({
success: z.boolean(),
message: z.string().optional(),
data: dataSchema.optional(),
error: z.string().optional(),
timestamp: z.string().datetime(),
});
const paginatedSchema = <T extends z.ZodTypeAny>(itemSchema: T) =>
z.object({
items: z.array(itemSchema),
total: z.number(),
page: z.number(),
limit: z.number(),
totalPages: z.number(),
hasNext: z.boolean(),
hasPrev: z.boolean(),
});
13.3 配置验证
const configSchema = z.object({
NODE_ENV: z.enum(["development", "production", "test"]),
PORT: z.coerce.number().int().positive().default(3000),
DATABASE_URL: z.string().url(),
DB_POOL_SIZE: z.coerce.number().int().positive().default(10),
REDIS_URL: z.string().url().optional(),
REDIS_TTL: z.coerce.number().int().positive().default(3600),
JWT_SECRET: z.string().min(32),
JWT_EXPIRES_IN: z.string().default("7d"),
AWS_ACCESS_KEY_ID: z.string().optional(),
AWS_SECRET_ACCESS_KEY: z.string().optional(),
})
.strict();
十四、最佳实践
14.1 代码组织
import { z } from 'zod';
export const baseUserSchema = z.object({
id: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
});
export const createUserSchema = z.object({
username: z.string().min(3).max(20),
email: z.string().email(),
password: z.string().min(6),
});
export const updateUserSchema = createUserSchema.partial();
export const userResponseSchema = baseUserSchema.merge(
z.object({
username: z.string(),
email: z.string().email(),
})
);
export type CreateUserInput = z.infer<typeof createUserSchema>;
export type UpdateUserInput = z.infer<typeof updateUserSchema>;
export type UserResponse = z.infer<typeof userResponseSchema>;
14.2 验证中间件
import { Request, Response, NextFunction } from 'express';
import { AnyZodObject, ZodError } from 'zod';
export const validate = (schema: AnyZodObject) =>
async (req: Request, res: Response, next: NextFunction) => {
try {
const data = {
body: req.body,
query: req.query,
params: req.params,
};
const validatedData = await schema.parseAsync(data);
req.validatedData = validatedData;
next();
} catch (error) {
if (error instanceof ZodError) {
return res.status(400).json({
success: false,
errors: error.errors.map(err => ({
path: err.path.join('.'),
message: err.message,
})),
});
}
next(error);
}
};
router.post('/users',
validate(createUserSchema),
userController.create
);
14.3 性能优化
- 缓存模式实例:避免重复创建模式
- 使用
z.lazy() 处理递归类型
- 避免深度嵌套:简化模式结构
- 使用
preprocess() 减少转换开销
十五、常见问题
15.1 如何处理可选字段?
const schema = z.object({
name: z.string().optional(),
age: z.number().optional(),
});
const partialSchema = schema.partial();
15.2 如何处理文件上传?
const fileSchema = z.object({
file: z.instanceof(File)
.refine((file) => file.size <= 5 * 1024 * 1024, {
message: "文件大小不能超过5MB",
})
.refine((file) => ['image/jpeg', 'image/png'].includes(file.type), {
message: "只支持 JPEG 和 PNG 格式",
}),
});
15.3 如何处理动态键?
const recordSchema = z.record(
z.string().uuid(),
z.number()
);
const dynamicSchema = z.object({}).catchall(z.string());
十六、与其他库集成
16.1 与 React Hook Form
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
const schema = z.object({
name: z.string().min(2),
email: z.string().email(),
});
const {
register,
handleSubmit,
formState: { errors },
} = useForm({
resolver: zodResolver(schema),
});
16.2 与 Express
import express from 'express';
import { z } from 'zod';
const app = express();
app.post('/api/users', async (req, res) => {
const schema = z.object({
name: z.string(),
email: z.string().email(),
});
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
errors: result.error.errors,
});
}
const userData = result.data;
});
快速参考表
加载中...