z.union:多选一
const StringOrNumber = z.union([z.string(), z.number()]); // string | number const Pet = z.union([ z.object({ kind: z.literal("dog"), breed: z.string() }), z.object({ kind: z.literal("cat"), indoor: z.boolean() }), ]); // 简写:.or() z.string().or(z.number());
union 会依次尝试每个成员——都失败才抛错。性能开销随成员数线性增。
z.discriminatedUnion(强烈推荐)
const Pet = z.discriminatedUnion("kind", [ z.object({ kind: z.literal("dog"), breed: z.string() }), z.object({ kind: z.literal("cat"), indoor: z.boolean() }), z.object({ kind: z.literal("fish"), waterType: z.enum(["salt", "fresh"]) }), ]);
和 union 的区别:先看 kind 字段决定走哪个分支,一次命中——性能好,错误信息好,推断出的 TS 类型是真正的 discriminated union。
| union | discriminatedUnion | |
|---|---|---|
| 性能 | O(n) 试错 | O(1) 跳转 |
| 错误信息 | 列出所有失败 | 只说正确分支的错 |
| TS 推断 | union | 真 discriminated |
| 要求 | 任意 | 每支都有同名字面量字段 |
业务 schema 首选 discriminatedUnion
API 响应通常都有
API 响应通常都有
type / kind / status 区分字段——只要有这样的 literal 字段,就一律用 discriminatedUnion。
z.intersection:合并
const Timestamps = z.object({ createdAt: z.date(), updatedAt: z.date(), }); const User = z.object({ id: z.string(), name: z.string() }); const UserWithTime = z.intersection(User, Timestamps); // { id, name, createdAt, updatedAt } // 等价 .and() User.and(Timestamps);
对象合并更建议用 .merge()(下一章),因为它能做字段覆盖;intersection 更通用(可以 intersect 非对象 schema)。
z.tuple:定长元组
const Point = z.tuple([z.number(), z.number()]); // [number, number] const RGBA = z.tuple([ z.number().int().min(0).max(255), z.number().int().min(0).max(255), z.number().int().min(0).max(255), z.number().min(0).max(1), ]); // 变长尾部 const LogEntry = z.tuple([z.date(), z.string()]).rest(z.any()); // [Date, string, ...any[]]
z.record:动态键对象
// v4 要求同时给 key schema 和 value schema const Scores = z.record(z.string(), z.number()); // Record<string, number> Scores.parse({ alice: 92, bob: 87 }); // 固定 key 类型 const I18n = z.record( z.enum(["en", "zh", "ja"]), z.string() ); // Record<"en" | "zh" | "ja", string>
z.map / z.set
z.map(z.string(), z.number()); // Map<string, number> z.set(z.string()); // Set<string> // 注意:输入必须是真的 Map / Set 实例,不是 plain object
z.lazy:递归类型
interface Category { id: string; name: string; children: Category[]; } const CategorySchema: z.ZodType<Category> = z.lazy(() => z.object({ id: z.string(), name: z.string(), children: z.array(CategorySchema), }) );
递归类型的限制
z.lazy 需要你显式标注 TS 类型(: z.ZodType<Category>)——TS 不能自己推断无限递归。另外没法用 z.infer,只能自己声明。
z.function:函数签名
const addSchema = z .function() .args(z.number(), z.number()) .returns(z.number()); const add = addSchema.parse((a: number, b: number) => a + b); add(1, 2); // Zod 自动在调用前校验 args,调用后校验 return add("1" as any, 2); // ZodError: args[0] expected number, received string
给第三方 callback 加运行时守门——比如允许用户传 plugin 函数时,校验它收到的参数和返回值。
z.promise
const schema = z.promise(z.string()); // Promise<string> const p = schema.parse(Promise.resolve("ok")); // 返回一个新的 Promise,会在 resolve 时校验值
optional vs nullable vs nullish vs default
| 用法 | 接受 undefined | 接受 null | TS 类型 |
|---|---|---|---|
.optional() | ✓ | ✗ | T | undefined |
.nullable() | ✗ | ✓ | T | null |
.nullish() | ✓ | ✓ | T | null | undefined |
.default(v) | ✓ → 变 v | ✗ | T |
.catch(v) | 错误时 → v | 错误时 → v | T |
z.string().catch("unknown"); // 校验失败不抛异常,返回 "unknown" // 适合"有值最好,没有也行"的场景,比如可选的 tracking ID
readonly
const schema = z.array(z.number()).readonly(); type T = z.infer<typeof schema>; // readonly number[] // 运行时也 Object.freeze
brand:标称类型
const UserId = z.string().uuid().brand<"UserId">(); type UserId = z.infer<typeof UserId>; // string & { [Symbol]: "UserId" } const PostId = z.string().uuid().brand<"PostId">(); type PostId = z.infer<typeof PostId>; function loadUser(id: UserId) { ... } loadUser("uuid-xyz"); // ❌ TS error: 需要 UserId 不是 string loadUser(PostId.parse("...")); // ❌ TS error: PostId 不是 UserId loadUser(UserId.parse("...")); // ✓
brand 让"同样是 string 但语义不同"的 ID 在编译期就不能互相替换——防止把 PostId 误传给吃 UserId 的函数。
实战:API 响应类型
// 真实场景:后端统一包装成 Result<T, E> const Ok = <T extends z.ZodTypeAny>(data: T) => z.object({ status: z.literal("ok"), data }); const Err = z.object({ status: z.literal("error"), code: z.string(), message: z.string(), }); const UserResponse = z.discriminatedUnion("status", [ Ok(UserSchema), Err, ]); const res = UserResponse.parse(await fetch(...).then(r => r.json())); if (res.status === "ok") { res.data; // User, TS 窄化完成 } else { res.code; // string }
JSON schema 对应表
| JSON 类型 | Zod |
|---|---|
| primitive | z.string/number/boolean/null |
| array | z.array(item) |
| tuple | z.tuple([...]) |
| object | z.object({...}) |
| additionalProperties | z.record(key, value) |
| oneOf | z.discriminatedUnion / union |
| allOf | z.intersection / .merge |
| enum | z.enum([...]) |
| const | z.literal(...) |
本章小结
discriminatedUnion是业务 API 首选(快、错误清晰、TS 窄化)tuple/record/map/set覆盖对象之外的容器z.lazy处理递归类型,但 TS 要显式标注optional/nullable/nullish/default/catch五种"可选语义"各有用法.brand()给字符串 ID 做标称类型,防止误传