Skip to content

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タプル型に固定

ベストプラクティスまとめ

  1. z.enum + as constをデフォルト選択肢に
    型安全性と簡潔さのバランスが最適
  2. 単一情報源による型生成
    z.infer<typeof MySchema>でスキーマから派生
  3. TypeScript enumは使用避ける
    特別な理由がない限りz.nativeEnumは使わない
  4. バリデーション前の変換
    大文字/小文字統一が必要な場合は.transform()を併用
ts
// 変換例:入力値を常に大文字に統一
const schema = z.object({
  paymentType: PaymentTypeSchema.transform(v => v.toUpperCase())
});

この方法により、UIからの入力値の揺れ(例: 'check' vs 'CHECK')を吸収できます。