TypeScriptの型推論エンジンは、単なる「静的検査のためのセーフティネット」ではない。それはビルド時に稼働する純粋関数型のメタプログラミング言語だ。
中でも `T extends U ? X : Y` という条件付き型(Conditional Types)は、入力された型を自在にねじ曲げ、意図した型へと収斂させるための最も強力なプリミティブである。
日々のコードレビューで、このような型定義を見かけることはないだろうか?
// ❌ どこにでもある、思考停止した“何でも許す”コード
function processValue(value: string | number): string | number {
if (typeof value === ‘string’) {
return value.toUpperCase();
}
return value 2;
}
const result = processValue(“hello”); // 型は string | number になってしまう
呼び出し側が `”hello”` というリテラル(`string`のサブタイプ)を渡しているにもかかわらず、戻り値の型がワイドな `string | number` に潰されてしまう。これではTypeScriptの恩恵の半分も受けられていない。
今回は、コンパイラの型推論エンジンをハックし、入力から出力までを完全に型安全に結びつける「条件付き型」の極意を、プロダクションコードレベルの設計パターンとともに伝授しよう。
—
1. 条件付き型の本質:型レベルの「三項演算子」ではない
まず、脳内のメンタルモデルをアップデートしてほしい。`T extends U ? X : Y` は、単なる型の三項演算子ではない。これは「型引数 `T` が `U` の部分型(Assignable)であるか」をコンパイル時に判定し、型空間の分岐を行うパターンマッチング機構である。
ここで重要なのが、`T` がジェネリクス(型変数)である場合、その判定は「遅延評価(Deferred Conditional Types)」されるという点だ。
type IsString
// 通常の評価
type A = IsString
type B = IsString
// ジェネリックな文脈での遅延評価
type Wrapper
value: T;
check: IsString
};
`Wrapper
—
2. 分散条件付き型(Distributive Conditional Types)の罠と制御
条件付き型の対象が裸の型パラメータ(naked type parameter)である場合、Union型が渡されると、コンパイラは勝手にそのUnionを分解し、一つずつ条件を適用して再度Unionに戻すという挙動をする。
言葉だけでは分かりにくいので、実務で頻出するバグの温床を見てみよう。
// 意図:T が string または number なら、そのまま返す。それ以外は never。
type StrictPrimitive
// 実行結果はどうなるか?
type Result = StrictPrimitive
// 予想: never (booleanが含まれているから)
// 実際: string !? なぜだ!?
なぜこのような挙動になるのか?(コンパイラの内部動作)
コンパイラは `StrictPrimitive
1. `StrictPrimitive
2. `StrictPrimitive
最終的にこれらを合流(Union)させるため、`string | never`となり、結果は `string` になってしまう。これはバグの温床になりやすい。
対策:タプルで包んで分散を阻止する
分散を意図しない場合、型パラメータを角括弧 `[]` で包み、「裸の型パラメータ」ではない状態(Non-distributive)にする。
// ✅ 正しい設計:分散を抑止した条件付き型
type StrictPrimitiveSafe
type ResultSafe = StrictPrimitiveSafe
// 結果: never (正しく弾かれる)
この「タプルによるハック」は、ライブラリの型定義や高度なコンポーネント設計において必須のテクニックである。
—
3. 実務で即効性を持つ:APIペイロードの動的型推論パターン
ここからが本番だ。非同期API連携やフロントエンドの状態管理において、「指定されたアクションの種類に応じて、引数と戻り値の型を完璧に自動推論させる」モジュールを設計する。
以下のコードは、コピペでそのままプロダクションに投入できる、堅牢なイベント/APIハンドラーの型設計である。
/
- 1. APIのドメインモデル定義
/
interface UserFetchPayload {
userId: string;
}
interface UserFetchResponse {
id: string;
name: string;
email: string;
}
interface PostCreatePayload {
title: string;
content: string;
}
interface PostCreateResponse {
postId: string;
createdAt: number;
}
// アクション定義の辞書型(Map)
type ApiSchema = {
‘user/fetch’: { payload: UserFetchPayload; response: UserFetchResponse };
‘post/create’: { payload: PostCreatePayload; response: PostCreateResponse };
};
/
- 2. 条件付き型を用いた型安全なクライアント関数
- 渡された Action に応じて、Payload と Response の型を完全に同期させる
/
type ApiPayload
type ApiResponse
// オーバーロードと条件付き型を組み合わせた究極の関数シグネチャ
async function executeApiRequest
action: T,
payload: ApiPayload
): Promise
// モックのフェッチ処理
console.log(`Executing [${action}] with`, payload);
// 実際にはここで fetch や axios を呼ぶ
return {} as ApiResponse
}
/
- 3. 実際の使用例(IDEの補完が完璧に効き、型ミスマッチはコンパイルエラーになる)
/
async function run() {
// ✅ 完璧な推論:payloadには userId が必須、戻り値は UserFetchResponse
const user = await executeApiRequest(‘user/fetch’, { userId: ‘123’ });
console.log(user.name); // string型として確定
// ❌ コンパイルエラー: PostCreatePayload に足りないプロパティがある、あるいは余計なプロパティがある
// const post = await executeApiRequest(‘post/create’, { userId: ‘123’ });
}
この設計の美しさは、拡張性(Open-Closed Principle)の高さにある。将来的にAPIのエンドポイントが増えた場合、`ApiSchema` に型定義を追加するだけで、すべての関数やコンポーネント側の型が自動的に追従する。手動で型キャスト(`as`)を行う必要は一切なくなる。
—
4. パフォーマンス上の注意点:型推論の重層化を防ぐ
チーフアーキテクトとして、最後にパフォーマンスの話をしなければならない。
TypeScriptの型推論エンジンは非常に強力だが、条件付き型のネストが深すぎたり、巨大なUnion型に対して不必要な条件分岐を行ったりすると、tscのコンパイル速度が著しく低下(爆発)する。
避けるべきアンチパターン
// ❌ 悪夢のような再帰的・多重条件付き型(コンパイラが泣く例)
type DeepModify
? { [K in keyof T]: DeepModify
: T extends string
? Uppercase
: T;
このような複雑怪奇な型は、大規模なコードベースにおいてIDEのインテリセンス(コード補完)を数秒間フリーズさせ、CIのビルド時間を何倍にも膨れ上がらせる原因になる。
チーフアーキテクトからの指針
1. 条件付き型は「浅く」保つ:ネストは原則2階層以内にする。
2. `infer` キーワードを賢く使う:戻り値や引数の型を抽出する際は、無駄な分岐を避け、直接 `infer` でパターンマッチさせる。
3. 複雑な型演算は一度隔離する:ドメインロジックの型と、UIコンポーネントの型を分離し、コンパイラの計算量を分散させる。
—
結びにかえて
条件付き型 `T extends U ? X : Y` は、TypeScriptを「単なる型チェックツール」から「ドメイン駆動設計をコードと型で完全に同期させるエンジン」へと昇華させる鍵である。
「なんとなく `any` や `as` で逃げる」コードを書くフェーズはもう卒業しよう。コンパイラの挙動を掌握し、型推論エンジンに仕事をさせることで、「バグの入り込む余地すらない美しいコードベース」をあなた自身の設計で構築してほしい。