在 Zod 中验证字符串字面量类型
问题描述
在 TypeScript 开发中,我们经常会定义字符串字面量联合类型来约束变量值范围,例如:
ts
export type PaymentType = 'CHECK' | 'DIRECT DEPOSIT' | 'MONEY ORDER';
但在运行时,我们需要验证输入数据是否符合该类型。当使用 Zod 进行模式验证时,直接使用 z.string()
无法精确匹配特定字符串值,而尝试使用标准 TypeScript 枚举或普通数组也会遇到类型匹配问题。
核心需求是:如何在 Zod 中验证输入值是否匹配预定义的字符串字面量联合类型?
解决方案
最佳实践:使用 z.enum
配合 as const
推荐使用 Zod 的 z.enum()
函数配合 TypeScript 的 as const
断言,这是最简洁高效的方法:
ts
// 定义值数组并使用 as const 声明为只读元组
export const PAYMENT_TYPES = ['CHECK', 'DIRECT DEPOSIT', 'MONEY ORDER'] as const;
// 创建 Zod 验证模式
export const PaymentSchema = z.enum(PAYMENT_TYPES);
// 创建对象 Schema
const schema = z.object({
paymentType: PaymentSchema,
});
// 提取 TypeScript 类型 (方法1)
export type PaymentType = z.infer<typeof PaymentSchema>;
// 提取 TypeScript 类型 (方法2,效果相同)
export type PaymentType = typeof PAYMENT_TYPES[number];
关键点说明:
as const
将数组转为只读元组,保留每个元素的字面量类型z.enum()
从元组创建验证器,确保仅接受指定值- 通过
z.infer
或数组索引可直接派生出原始联合类型
其他可行方案
方案1:联合 z.literal
验证器
当需要更复杂的验证逻辑时,可以使用基础API组合实现:
ts
const PaymentSchema = z.union([
z.literal('CHECK'),
z.literal('DIRECT DEPOSIT'),
z.literal('MONEY ORDER')
]);
const schema = z.object({
paymentType: PaymentSchema,
});
适用场景
- 需要为不同值应用额外验证规则
- 验证逻辑超出简单枚举范围时使用
方案2:使用原生枚举 (z.nativeEnum
)
如果项目中已存在 TypeScript 枚举定义:
ts
enum PaymentType {
Check = 'CHECK',
DirectDeposit = 'DIRECT DEPOSIT',
MoneyOrder = 'MONEY ORDER'
}
const schema = z.object({
paymentType: z.nativeEnum(PaymentType),
});
注意事项
TypeScript 枚举存在一些争议性问题:
- 编译后会生成实际对象
- 可能导致命名空间污染
- 与纯字符串类型存在兼容差异
因此优先推荐 z.enum
+ as const
方案
原理剖析
as const
的关键作用
没有 as const
时,普通数组会被推断为 string[]
:
ts
// ❌ 错误用法:类型被拓宽为 string[]
const TYPES = ['A', 'B'];
// => 类型为 string[]
添加 as const
后,TypeScript 保留精确字面量类型:
ts
// ✅ 正确用法:保留精确值
const TYPES = ['A', 'B'] as const;
// => 类型为 readonly ["A", "B"]
类型兼容性处理
使用 as const
+ z.enum
方案后:
- 运行时:Zod 严格校验输入值是否在数组中
- 编译时:推导类型与原始联合类型完全一致
- 无额外运行时开销,编译后会被擦除类型标注
完整示例
实际应用场景下的完整示意代码:
ts
import { z } from 'zod';
// 1. 定义字面量值数组
const STATUS_VALUES = ['PENDING', 'APPROVED', 'REJECTED'] as const;
// 2. 创建 Zod 验证器
const StatusSchema = z.enum(STATUS_VALUES);
// 3. 创建对象模式
const RequestSchema = z.object({
id: z.string(),
status: StatusSchema,
});
// 4. 类型推导
type StatusType = z.infer<typeof StatusSchema>;
// => 'PENDING' | 'APPROVED' | 'REJECTED'
// 实际验证使用
RequestSchema.parse({
id: 'req-123',
status: 'PENDING' // 通过验证
});
RequestSchema.parse({
id: 'req-456',
status: 'INVALID' // ZodError: 无效枚举值
});
总结
- 使用
as const + z.enum()
是最佳验证方式- 同时满足运行/编译时类型约束
- 避免 TypeScript 枚举的潜在问题
- 次要方案
z.union/z.literal
适用于特殊场景 z.nativeEnum
仅在已有TS枚举结构时推荐
通过将值数组声明为 as const
并使用 z.enum
创建验证器,可以实现字符串字面量类型的无缝验证,同时保持与原始类型定义的完全兼容。