TypeScript型システムを「コンパイル時ガード」として使い倒す:Zod無き世界での型レベル・バリデーション
TypeScriptの型システムは、単なる「静的検査ツール」ではない。それは、コンパイル時に実行される「純粋関数型の定理証明器」である。
多くのエンジニアはZodのようなランタイムバリデーションライブラリに依存し、型と実行時コードの二重管理に疲弊している。だが、TypeScriptの型システムを深く理解すれば、コンパイル時にコードの正当性を証明し、ランタイムの負荷を最小化する「ゼロコスト・バリデーション」の実装が可能になる。
今日は、コンパイラの評価エンジンをハックし、型レベルで制約を強制する極限の手法を解説する。
—
1. なぜ「型による制約」が最強の防壁なのか
ランタイムバリデーションは、イベントループのキューを消費し、メモリを浪費する。特にNode.jsのようなシングルスレッドモデルにおいて、バリデーションロジックの多重実行はボトルネックになり得る。
対して、型による制約は「コンパイラが計算を終えた瞬間に消滅する」。これは実行時のメモリレイアウトにも影響を与えず、CPUサイクルを一切消費しない。我々の目的は、境界値や形式の正しさを「型」というメタデータの中に閉じ込め、不正な値がランタイムに到達する前にコンパイラという名の「防壁」で弾くことにある。
—
2. テンプレートリテラル型による「文字列制約」の具現化
例えば、ユーザーIDが `USR-XXXX` という形式である必要がある場合、多くの者は正規表現でランタイムチェックを行う。しかし、型システムを使えばこうなる。
/
- テンプレートリテラル型を利用した「IDバリデータ」
- コンパイラは、この型が適合しない全てのケースをビルドエラーとして弾く。
/
type Brand
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 Range type Score = Range<1, 10>; const s: Score = 11; // コンパイルエラー: Type ’11’ is not assignable to type ‘Score’ この手法は、コンパイラの再帰深さを利用して型レベルで数値を生成している。これは、コンパイル時に型検査器が「この値は1〜10の範囲にあるか?」を再帰的にマッチングさせることで実現される。ランタイムには定数しか残らない。 — ここまでの実装で、読者の諸君は気づいただろうか。 関数型プログラミングの知見を導入し、バリデーション済みのデータのみを受け付ける「型」を定義することで、不正な状態(Invalid State)を表現不可能な設計にするのが、最も堅牢なセキュリティプラットフォームである。 type ValidatedData = { // バリデーションを通過したものだけが、この型を持つ — 多くのエンジニアにとって、TypeScriptの型は「オートコンプリートのためのサジェスト」に過ぎない。だが、真のシニアエンジニアにとって、それはプログラムの振る舞いを記述する最も厳格な言語だ。 Zodを使うなと言っているのではない。しかし、ランタイムバリデーションに頼り切り、型を単なる「ヒント」として扱うのは、TypeScriptのポテンシャルの5%も引き出せていない。 型システムで記述可能な制約は、全て型システムに委ねろ。 君たちのコードが、コンパイルエラーによって守られることを願っている。
type Enumerate
Acc[‘length’] extends N ? Acc[number] : Enumerate4. セキュリティとパフォーマンスの真実
我々が行っているのは、「ランタイムの責務をコンパイルタイムにシフトさせる」というアーキテクチャ上の転換だ。
究極の防御:型による「状態遷移の強制」
readonly status: ‘validated’;
readonly payload: unknown;
};
function process(data: ValidatedData) {
// ここには必ず安全なデータのみが到達する
}結論:型は「ドキュメント」ではなく「契約」である
実行時に動くコードは、最小限であるほど美しい。それが、現代のアーキテクチャにおける「正義」である。