Zodによる文字列リテラル型の検証方法
実践シナリオ
TypeScriptで定義された文字列リテラル型をZodスキーマで検証したいケースは頻繁に発生します。例えば支払い方法の選択肢など、固定された文字列値のセットを扱う際に有効です。
問題:文字列リテラル型の検証
TypeScriptの文字列リ�ラル型がある場合:
ts
export type PaymentType = 'CHECK' | 'DIRECT DEPOSIT' | 'MONEY ORDER';
この型に合致する値のみを検証するZodスキーマを作成したいところ、単純にz.string()
を使用すると全ての文字列を許容してしまい、型安全性が損なわれます。
最適な解決方法
1. z.enum
を使った基本アプローチ(推奨)
最も簡潔で型安全な方法です。
ts
import { z } from 'zod';
// リテラル値を配列で定義(as const必須)
const PAYMENT_TYPES = ['CHECK', 'DIRECT DEPOSIT', 'MONEY ORDER'] as const;
// Zodスキーマ作成
const PaymentTypeSchema = z.enum(PAYMENT_TYPES);
// 型定義(Zod推論から生成)
type PaymentType = z.infer<typeof PaymentTypeSchema>;
// 使用例
const schema = z.object({
paymentType: PaymentTypeSchema, // 特定の文字列のみ許可
});
重要なポイント
配列定義時にas const
(constアサーション)を必ず付与してください。これにより配列がリテラルのタプル型と認識され、型安全性が確保されます。
as const
の役割
TypeScript 3.4で導入されたconstアサーションas const
は:
- 配列を
readonly
タプル型として扱う - 値の型を文字列リテラルに固定する(例:
'CHECK'
ではなくstring
と推論されるのを防ぐ) - 型と実行時の値を完全同期可能にする
2. z.union
を使った代替方法
個々のリテラルを明示的に指定する手法です。
ts
const PaymentTypeSchema = z.union([
z.literal('CHECK'),
z.literal('DIRECT DEPOSIT'),
z.literal('MONEY ORDER')
]);
使い分け基準
z.union
方式は:
- 値ごとに異なるバリデーションを追加できる場合に有用
- コードが冗長になるので通常は
z.enum
推奨
3. TypeScript enumとの連携
既存のTypeScript enumを利用するケース用です。
ts
enum PaymentTypeEnum {
Check = 'CHECK',
DirectDeposit = 'DIRECT DEPOSIT',
MoneyOrder = 'MONEY ORDER'
}
const schema = z.object({
paymentType: z.nativeEnum(PaymentTypeEnum)
});
TypeScript enumの問題点
- 数値enumでの逆マッピングによる意図しないアクセス
- ツリーシェイキングが効きにくい
- 追加機能に比べメリットが少ない
応用:スキーマと型の連携
定義したスキーマからTypeScript型を生成する方法:
ts
// スキーマの推論から型生成
type PaymentType = z.infer<typeof PaymentTypeSchema>;
// = 下記型と同等
// type PaymentType = 'CHECK' | 'DIRECT DEPOSIT' | 'MONEY ORDER'
この手法により、単一の情報源から型とバリデーションが同期され、メンテナンスが容易になります。
よくあるエラーと解決策
エラー:Argument of type 'string[]' is not assignable to parameter of type [...]
as const
なしで配列を渡した場合に発生します。
解決策:
ts
// 間違い
const types = ['A', 'B']; // string[]型になる
// 正解
const types = ['A', 'B'] as const; // readonlyタプル型に固定
ベストプラクティスまとめ
z.enum + as const
をデフォルト選択肢に
型安全性と簡潔さのバランスが最適- 単一情報源による型生成
z.infer<typeof MySchema>
でスキーマから派生 - TypeScript enumは使用避ける
特別な理由がない限りz.nativeEnum
は使わない - バリデーション前の変換
大文字/小文字統一が必要な場合は.transform()
を併用
ts
// 変換例:入力値を常に大文字に統一
const schema = z.object({
paymentType: PaymentTypeSchema.transform(v => v.toUpperCase())
});
この方法により、UIからの入力値の揺れ(例: 'check' vs 'CHECK')を吸収できます。