TypeScriptの「依存型」を掌握せよ:引数連動による型安全性の極致
フロントエンドの現場において、`any` や型アサーションで誤魔化されたコードは、未来の自分に対する「負債」でしかない。特に、ある引数の値によって、別の引数に渡すべき型が動的に決定されるような「依存関係のあるAPI」を設計する際、甘い型定義はランタイムエラーの温床となる。
今回は、TypeScriptのジェネリクス制約(`extends`)を駆使し、「第一引数に依存して第二引数の型を絞り込む」という、実務で必須のパターンを伝授する。
—
1. なぜ「静的な型定義」では不十分なのか
例えば、APIクライアントやイベントハンドラで、特定の「種別(Type)」に応じて「ペイロード(Data)」が変わるインターフェースを想像してほしい。
// アンチパターン:型が緩すぎてバグを誘発する
function sendEvent(type: string, data: any) {
// ここで type が ‘click’ なのに data が { page: number } を期待している場合、
// 実行時に例外が投げられるまで誰も気づけない。
}
このコードの何が悪いのか? それは「型システムが引数間の相関関係を認識していない」点にある。TypeScriptのコンパイラは、`type`と`data`を独立した変数として評価してしまうからだ。
2. ジェネリクス制約による型連動の構築
この問題を解決するには、`type`の値を型として抽出し、それをキーにして`data`を制約する「マップ型(Mapped Types)」と「Indexed Access Types」の合わせ技が最適解となる。
実装パターン:イベントペイロードの完全型安全化
// 1. 各イベントのデータ定義を単一のソースとして保持する
interface EventMap {
‘click’: { x: number; y: number };
‘hover’: { elementId: string };
‘submit’: { formId: string; timestamp: number };
}
// 2. K extends keyof EventMap で、第一引数を EventMap のキーに限定する
// 3. 第二引数を EventMap[K] で参照することで、動的に型を連動させる
function trackEvent
type: K,
data: EventMap[K]
): void {
console.log(`Tracking ${type}:`, data);
}
// 恩恵:補完が効き、不正な組み合わせは即座にコンパイルエラーになる
trackEvent(‘click’, { x: 10, y: 20 }); // OK
// trackEvent(‘click’, { elementId: ‘btn’ }); // コンパイルエラー!
この実装において、コンパイラは`K`という型変数を介して、`type`と`data`の間に強力な「依存性」を確立する。これがTypeScriptの真骨頂だ。
—
3. パフォーマンスとコンパイル速度への配慮
「型定義が複雑になりすぎると、IDEのレスポンスやビルド時間が悪化する」という懸念を持つかもしれない。確かに、過剰な条件付き型(Conditional Types)のネストはコンパイラの計算量を増大させる。
しかし、今回紹介した `keyof` を用いたマップ型アクセスは、TypeScriptの型評価器の中でも極めて高速に処理される部類だ。以下の点に注意すれば、大規模なコードベースでも全く問題ない。
- 単一のインターフェースに集約する: `EventMap` のような「型辞書」を一箇所で定義することで、型評価のキャッシュ効率が上がる。
- 複雑な条件分岐を避ける: 無理に `infer` を多用せず、シンプルなマップ型で済む設計を心がける。
—
4. 実戦でのさらなる応用:Discriminated Unions(判別可能なユニオン)
もしAPIが「APIのレスポンス型」のように、より複雑な階層を持つ場合は、`Discriminated Unions`と組み合わせることでさらに堅牢になる。
type APIResponse =
| { status: ‘success’; data: string }
| { status: ‘error’; message: string; code: number };
// 判別子(status)をキーにして、それ以外のプロパティを絞り込む
function handleResponse(res: APIResponse) {
if (res.status === ‘success’) {
console.log(res.data); // ここでは ‘data’ が存在することが確定している
}
}
結論:型は「ドキュメント」ではなく「契約」である
ジェネリクス制約を用いた型設計は、単なる「補完を出すためのツール」ではない。「どの組み合わせが許容され、どの組み合わせが禁止されているか」という仕様を、コンパイラという究極のレビュアーに強制する契約書だ。
コードレビューで「なぜ `any` なのか?」と指摘する前に、まずはこの「連動する型設計」を導入してほしい。コードが意図を語り始めれば、コメントアウトに頼る古い開発手法から脱却できるはずだ。
堅牢なフロントエンド構築には、TypeScriptの型システムへの深い敬意が必要だ。さあ、今すぐプロジェクトの型定義を再構築してほしい。