Skip to content

在 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];

关键点说明:

  1. as const 将数组转为只读元组,保留每个元素的字面量类型
  2. z.enum() 从元组创建验证器,确保仅接受指定值
  3. 通过 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 枚举存在一些争议性问题:

  1. 编译后会生成实际对象
  2. 可能导致命名空间污染
  3. 与纯字符串类型存在兼容差异

因此优先推荐 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: 无效枚举值
});

总结

  1. 使用 as const + z.enum() 是最佳验证方式
    • 同时满足运行/编译时类型约束
    • 避免 TypeScript 枚举的潜在问题
  2. 次要方案 z.union/z.literal 适用于特殊场景
  3. z.nativeEnum 仅在已有TS枚举结构时推荐

通过将值数组声明为 as const 并使用 z.enum 创建验证器,可以实现字符串字面量类型的无缝验证,同时保持与原始类型定义的完全兼容。