【テクニカル・上級編】Type Aliasを用いた「型レベルのバリデーション」:Zodを使わずに型で制約をかける – TypeScript コア・型システムの基礎解析バイブル

TypeScript型システムを「コンパイル時ガード」として使い倒す:Zod無き世界での型レベル・バリデーション

TypeScriptの型システムは、単なる「静的検査ツール」ではない。それは、コンパイル時に実行される「純粋関数型の定理証明器」である。

多くのエンジニアはZodのようなランタイムバリデーションライブラリに依存し、型と実行時コードの二重管理に疲弊している。だが、TypeScriptの型システムを深く理解すれば、コンパイル時にコードの正当性を証明し、ランタイムの負荷を最小化する「ゼロコスト・バリデーション」の実装が可能になる。

今日は、コンパイラの評価エンジンをハックし、型レベルで制約を強制する極限の手法を解説する。

—

1. なぜ「型による制約」が最強の防壁なのか

ランタイムバリデーションは、イベントループのキューを消費し、メモリを浪費する。特にNode.jsのようなシングルスレッドモデルにおいて、バリデーションロジックの多重実行はボトルネックになり得る。

対して、型による制約は「コンパイラが計算を終えた瞬間に消滅する」。これは実行時のメモリレイアウトにも影響を与えず、CPUサイクルを一切消費しない。我々の目的は、境界値や形式の正しさを「型」というメタデータの中に閉じ込め、不正な値がランタイムに到達する前にコンパイラという名の「防壁」で弾くことにある。

—

2. テンプレートリテラル型による「文字列制約」の具現化

例えば、ユーザーIDが `USR-XXXX` という形式である必要がある場合、多くの者は正規表現でランタイムチェックを行う。しかし、型システムを使えばこうなる。

/

  • テンプレートリテラル型を利用した「IDバリデータ」
  • コンパイラは、この型が適合しない全てのケースをビルドエラーとして弾く。

/
type Brand = T & { readonly __brand: B };
type UserID = Brand<`USR-${string}`, 'UserID'>;

function validateUserID(id: string): UserID {
if (!/^USR-\d{4}$/.test(id)) {
throw new Error(‘Invalid UserID format’);
}
return id as UserID;
}

// 使用例
const validId = validateUserID(‘USR-1234’); // 正常
const invalidId = validateUserID(‘ABC-1234’); // ランタイムで弾かれるだけでなく、型システム上で識別可能

ここで重要なのは、`Brand` 型による「名目的な型付け(Nominal Typing)」のシミュレーションだ。TSは構造的部分型を採用しているが、この `__brand` プロパティを付与することで、不適切な型が代入されることを防ぐ「型レベルのガードレール」を構築できる。

—

3. 条件型(Conditional Types)による再帰的バリデーション

より高度な制約、例えば「配列の長さが3以上」や「特定の数値範囲」を型で強制したい場合、再帰的条件型を駆使する。

/

  • 数値の範囲を型で強制する(例: 1以上10以下)

/
type Enumerate =
Acc[‘length’] extends N ? Acc[number] : Enumerate

type Range = Exclude, Enumerate> | To;

type Score = Range<1, 10>;

const s: Score = 11; // コンパイルエラー: Type ’11’ is not assignable to type ‘Score’

この手法は、コンパイラの再帰深さを利用して型レベルで数値を生成している。これは、コンパイル時に型検査器が「この値は1〜10の範囲にあるか?」を再帰的にマッチングさせることで実現される。ランタイムには定数しか残らない。

—

4. セキュリティとパフォーマンスの真実

ここまでの実装で、読者の諸君は気づいただろうか。
我々が行っているのは、「ランタイムの責務をコンパイルタイムにシフトさせる」というアーキテクチャ上の転換だ。

  • イベントループへの影響: ゼロ。バリデーションロジックの肥大化による `RangeError: Maximum call stack size exceeded` や、ヒープメモリの圧迫を回避できる。
  • コンパイラの挙動: TypeScriptの型検査器は `Type Mapping` や `Conditional Types` が深すぎるとパフォーマンスが低下する。だが、これは開発環境の話であり、本番環境のバイナリ(JS)には一切の影響を及ぼさない。

究極の防御:型による「状態遷移の強制」

関数型プログラミングの知見を導入し、バリデーション済みのデータのみを受け付ける「型」を定義することで、不正な状態(Invalid State)を表現不可能な設計にするのが、最も堅牢なセキュリティプラットフォームである。

type ValidatedData = {
readonly status: ‘validated’;
readonly payload: unknown;
};

// バリデーションを通過したものだけが、この型を持つ
function process(data: ValidatedData) {
// ここには必ず安全なデータのみが到達する
}

—

結論:型は「ドキュメント」ではなく「契約」である

多くのエンジニアにとって、TypeScriptの型は「オートコンプリートのためのサジェスト」に過ぎない。だが、真のシニアエンジニアにとって、それはプログラムの振る舞いを記述する最も厳格な言語だ。

Zodを使うなと言っているのではない。しかし、ランタイムバリデーションに頼り切り、型を単なる「ヒント」として扱うのは、TypeScriptのポテンシャルの5%も引き出せていない。

型システムで記述可能な制約は、全て型システムに委ねろ。
実行時に動くコードは、最小限であるほど美しい。それが、現代のアーキテクチャにおける「正義」である。

君たちのコードが、コンパイルエラーによって守られることを願っている。

タイトルとURLをコピーしました