指南 · 基础
版本基线 Zod 4。本篇把「会装会用」推进到「懂构件与语义」:schema 全景、对象额外键策略、可选/可空/默认值、校验 vs 转换、类型推导。标注 ⚠️ 处为 v3 → v4 差异。
一、schema 全景
Zod 的一切都从「schema 工厂函数」开始,链式方法在其上叠加约束,每步返回新的不可变 schema:
import * as z from "zod";
// 原语
z.string(); z.number(); z.boolean(); z.bigint(); z.date(); z.symbol();
z.null(); z.undefined(); z.any(); z.unknown(); z.never();
// 复合
z.object({ name: z.string(), age: z.number() }); // 对象
z.array(z.string()); // 数组
z.tuple([z.string(), z.number()]); // 定长元组
z.union([z.string(), z.number()]); // 联合
z.record(z.string(), z.number()); // ⚠️ v4 必须两参
z.map(z.string(), z.number()); // Map
z.set(z.number()); // Set
// 字面量 / 枚举
z.literal("admin"); // 字面量
z.enum(["light", "dark"]); // 枚举(联合字面量)
z.optional(z.string()); // 可选(= .optional())
z.nullable(z.string()); // 可空(= .nullable())⚠️ v4 中
z.enum([...])被重载:既能表示字符串字面量集合,也能直接吃 TS 原生enum,因此z.nativeEnum()被弃用,统一用z.enum()。
二、对象与额外键策略
z.object 对「schema 未声明的多余键」有三种处理,v4 用不同工厂表达(而非 v3 的 .strict()/.passthrough() 方法链):
| 工厂 | 多余键处理 |
|---|---|
z.object({...}) | 默认:剥离(strip),校验通过但输出不含多余键 |
z.strictObject({...}) | 有多余键就报错(⚠️ 取代 .strict()) |
z.looseObject({...}) | 保留多余键(⚠️ 取代 .passthrough()) |
z.object({...}).catchall(s) | 给所有未声明键的值指定 schema |
z.object({ name: z.string() }).parse({ name: "a", extra: 1 });
// => { name: "a" }(extra 被剥离)
z.strictObject({ name: z.string() }).parse({ name: "a", extra: 1 });
// => 抛 ZodError(unrecognized_keys)
z.object({ id: z.number() }).catchall(z.string());
// id 必须是 number,其余任意键的值必须是 string三、可选、可空与默认值
z.string().optional(); // 额外接受 undefined(推导可选属性)
z.string().nullable(); // 额外接受 null
z.string().nullish(); // 额外接受 null 和 undefined
z.string().default("tuna"); // 缺省(undefined)时填充
z.number().default(Math.random); // 也可传函数,每次解析重新求值
z.number().catch(42); // 校验失败时回退三者分工要分清:optional 只管 undefined、nullable 只管 null、nullish 两者都管。
⚠️ v4 默认值短路:
.default(v)在输入为undefined时直接返回 v、不再走解析,所以v必须可赋给「输出类型」。若想让默认值先经过解析(v3 旧行为),用.prefault():tsz.string().transform((s) => s.length).default(0); // 输入 undefined → 0 z.string().transform((s) => s.length).prefault("tuna"); // 输入 undefined → 4另外
.default()只对undefined生效;.catch()才是「值非法时兜底」。
四、校验 vs 转换
Zod 的链式方法分两类,理解这点是写好 schema 的关键:
- 校验:只判断是否合法、不改值。如
.min().max().regex()、顶层z.email()z.url(),以及自定义的.refine()。 - 转换:改变值(可改类型),改变 schema 的输出类型。如
.trim().toLowerCase()、z.coerce.*,以及自定义的.transform()。
// 校验:字符串、去空白、是邮箱
const Email = z.email().trim(); // trim 是转换,email 是校验
// 转换:字符串 → 数字(输入 string,输出 number)
const Num = z.string().transform((s) => Number(s));
// 自定义校验(≈ 布尔判定)
const Username = z.string().refine(
(s) => /^[a-z0-9_]+$/.test(s),
{ error: "只能用小写字母、数字、下划线" }
);
// 自定义转换
const Slug = z.string().transform((s) => s.toLowerCase().replace(/\s+/g, "-"));
refine返回布尔(true 通过),transform返回新值。一个判定「行不行」,一个负责「变成什么」。
五、强制转换 coerce
来自 URL query、表单、环境变量的输入天生是字符串。z.coerce.* 先强制转换再校验:
z.coerce.number().parse("42"); // => 42(先 Number("42"))
z.coerce.boolean().parse(""); // => false(Boolean(""))
z.coerce.date().parse("2026-01-01"); // => Date 对象
// 对比:普通 z.number() 收到 "42" 会报 invalid_type⚠️ v4 中所有
z.coerce.*的输入类型是unknown(v3 是具体类型)。另注意z.coerce.boolean()用Boolean(input)——任何非空字符串都为true;要按 "true"/"false" 语义解析用z.stringbool()。
六、类型推导:infer / input / output
const Schema = z.string().transform((s) => s.length);
type In = z.input<typeof Schema>; // string(校验前)
type Out = z.output<typeof Schema>; // number(校验后)
type T = z.infer<typeof Schema>; // number(= output)z.infer:日常最常用,等于z.output,是「校验后的业务类型」。z.input:校验前的输入类型,少数场景用(如表单原始值类型)。- 二者何时不同:有
transform、coerce、带默认值的default时。纯校验(如z.email().min(5))则输入输出一致。
进入 指南 · 进阶:refine / superRefine / transform / pipe 深入、判别联合、递归 schema、错误处理与生态接入。