
本文介绍如何在 Zod 中实现类似 Yup 的条件必填验证(如当 is_change_part === 'Yes' 时 barcode_old 必填),重点推荐使用 z.discriminatedUnion 构建类型安全、声明式且可静态推导的联合模式。
本文介绍如何在 zod 中实现类似 yup 的条件必填验证(如当 `is_change_part === 'yes'` 时 `barcode_old` 必填),重点推荐使用 `z.discriminatedunion` 构建类型安全、声明式且可静态推导的联合模式。
在 Zod 中实现字段级条件验证,核心目标是:让类型系统能准确反映运行时约束,并避免手动 superRefine 带来的类型丢失与冗余逻辑。你最初尝试的 superRefine 方案虽可行,但存在明显缺陷:它将验证逻辑移至运行时,导致 TypeScript 无法在编译期推导 barcode_old 在 'Yes' 分支下的非空性,从而削弱类型安全性与开发体验。
✅ 推荐方案:使用 z.discriminatedUnion 实现类型驱动的条件模式
该方法通过将不同业务状态(如 is_change_part: "Yes" 和 "No")建模为独立的、结构明确的对象 Schema,再用 discriminatedUnion 统一聚合,天然支持:
- 编译期类型推导(例如:
data.is_change_part === 'Yes'时,data.barcode_old自动为string,非string | undefined); - 零运行时开销的声明式验证;
- 清晰的语义表达与可维护性。
import { z } from 'zod';
const stringRequired = z.string().min(1);
// 主体 Schema:定义所有字段的基础结构(含可选字段)
const baseSchema = z.object({
serial_number: stringRequired,
barcode_old: z.string().optional(), // 默认可选
is_change_part: z.enum(['Yes', 'No']), // 强制枚举,提升类型安全
category: stringRequired,
sub_category: stringRequired,
});
// 条件分支 Schema:按 is_change_part 值精确建模
const schemaWhenYes = z.object({
is_change_part: z.literal('Yes'),
barcode_old: stringRequired, // ✅ 此处强制必填,类型系统自动识别
});
const schemaWhenNo = z.object({
is_change_part: z.literal('No'),
// barcode_old 无需在此声明 —— 它在 baseSchema 中已定义为 optional,且此分支不约束其存在性
});
// 合并为带判别逻辑的联合 Schema
export const Schema = z.discriminatedUnion('is_change_part', [
schemaWhenYes,
schemaWhenNo,
]).and(baseSchema); // 使用 .and() 合并基础字段,确保 serial_number 等始终存在
⚠️ 注意事项:
-
z.discriminatedUnion要求判别字段(此处为'is_change_part')在每个分支中必须是 字面量类型(如z.literal('Yes')),不可用z.string()替代,否则联合类型会退化为any。 -
.and(baseSchema)是关键:它将公共字段(如serial_number,category)注入到每个分支中,避免重复定义,同时保留所有必需字段的约束。 - 若需额外校验(如
is_change_part只能为'Yes' | 'No'),z.enum(['Yes', 'No'])已在baseSchema中保证,无需重复。
? 进阶提示:对于更复杂的多层条件(如嵌套字段或多个控制字段),可组合 z.union + z.object 或封装为自定义 z.custom(),但 discriminatedUnion 应作为首选——它是最符合 Zod 设计哲学(“Schema 即类型”)的原生解决方案。
总结:告别 superRefine 的手动 addIssue,拥抱 discriminatedUnion——它让条件验证从“运行时检查”升级为“编译期契约”,真正实现类型即文档、Schema 即规范。










