Conditional Typesで型レベルの条件分岐を極める:コンパイルタイムを支配する高度型設計
テックリードの私だ。コードレビューの現場で、未だに「とりあえず `any` を置いておく」「無駄に複雑な `as` キャストで型エラーをねじ伏せる」というコードを見かけるたびに、私は深い絶望と、同時に改善への闘志を覚える。
TypeScriptの真髄は、ランタイムの安全性を担保することだけではない。「型システムというもう一つの言語」を用いて、コンパイル時にあらゆる可能性を網羅し、不可能な状態を型レベルで不可能にする(Make impossible states unrepresentable)ことにある。
今回は、その最先鋒である Conditional Types(条件付き型) を取り上げる。単なるリファレンスの引き写しではない。コンパイラがどう型を評価し、パフォーマンスと保守性を両立させたプロダクションコードをどう組み上げるのか、その極限の知見を授けよう。
—
1. 条件付き型の本質:なぜ三項演算子なのか
TypeScriptの Conditional Types は、一見するとJavaScriptの三項演算子に似ている。
T extends U ? X : Y
しかし、この評価メカニズムの裏側を理解している者は少ない。これは単なるif文ではない。集合論における「部分型関係(Subtyping)」の判定である。
`T` が `U` の部分集合(assignable)であるならば `X` を返し、そうでなければ `Y` を返す。この純粋な数学的写像をコンパイラは静的に解決している。
分配条件付き型(Distributive Conditional Types)の罠と恩恵
ジェネリック型に Conditional Types を適用する際、もっとも注意すべき、そして強力な挙動が 「分配則」 だ。
type ToArray
// 以下の評価はどうなるか?
type Result = ToArray
// 答え: string[] | number[] (配列のユニオン)
// ×: (string | number)[] ではない!
ジェネリック型パラメータが裸(naked)の型として条件の左側にある場合、TypeScriptは自動的にユニオンを分解し、要素ごとに条件を適用してから再度ユニオンとして再構築する。
これが「分配条件付き型」だ。この挙動を意図的に殺したい(ユニオンをそのまま一塊として評価したい)場合は、以下のようにタプルでラップする。
type ToArrayNonDistributive
type ResultStrict = ToArrayNonDistributive
// 答え: (string | number)[]
この違いを理解していないと、非同期APIのレスポンス型や状態管理のステート定義で予期せぬバグを踏むことになる。
—
2. 実務で即座に使えるプロダクションコード:型安全なAPIクライアントの構築
では、実務の現場でどう応用するのか。
「エンドポイントのパスに応じて、リクエストペイロードとレスポンスの型が完全に一意に決まるAPIクライアント」を例に取ろう。
凡庸なコードであれば、メソッドオーバーロードを何十行も書き連ねるか、`any` で型を破壊する。しかし、Conditional Typesを使いこなす我々であれば、こう書く。
// — ドメイン定義 —
type ApiSchema = {
‘/users’: {
get: { res: Array<{ id: string; name: string }> };
post: { req: { name: string; email: string }; res: { id: string } };
};
‘/posts’: {
get: { query: { limit?: number }; res: Array<{ id: string; title: string }> };
post: { req: { title: string; body: string; userId: string }; res: { id: string } };
};
};
type HttpMethod = ‘get’ | ‘post’;
// — 条件付き型による高度な推論エンジン —
type RequestPayload<
TPath extends keyof ApiSchema,
TMethod extends HttpMethod
> = TMethod extends ‘post’
? ApiSchema[TPath] extends { post: { req: infer R } }
? R
: never
: ApiSchema[TPath] extends { get: { query: infer Q } }
? Q
: void; // パラメータが不要な場合は void
type ResponsePayload<
TPath extends keyof ApiSchema,
TMethod extends HttpMethod
> = ApiSchema[TPath] extends Record
? Res
: never;
// — 型安全なクライアント実装 —
class TypedApiClient {
async request<
TPath extends keyof ApiSchema,
TMethod extends HttpMethod
>(
path: TPath,
method: TMethod,
…args: RequestPayload
? []
: [payload: RequestPayload
): Promise
const payload = args[0];
const response = await fetch(path, {
method: method.toUpperCase(),
body: payload ? JSON.stringify(payload) : undefined,
});
return response.json();
}
}
// ==========================================
// 使用例(IDEの補完が完璧に効く)
// ==========================================
const client = new TypedApiClient();
// OK: /posts の POST には req が必須
await client.request(‘/posts’, ‘post’, {
title: ‘Advanced TypeScript’,
body: ‘Conditional types are awesome.’,
userId: ‘user_123’,
});
// コンパイルエラー: /users の GET にペイロードを渡そうとすると怒られる
// await client.request(‘/users’, ‘get’, { invalid: true });
// OK: /users の GET は引数なしで呼べ、レスポンスはユーザー配列に型推論される
const users = await client.request(‘/users’, ‘get’);
このコードの美しさは、「間違ったAPIリクエストの組み合わせを、コンパイルエラーとして完全に封じ込めている点」にある。開発者がエンドポイントとメソッドを指定した瞬間、IDEは必要な引数を正確に要求し、返り値の型を自動で確定させる。
—
3. パフォーマンス上の注意点:コンパイラを殺さないために
ここでチーフアーキテクトとして、厳格な警告をしておかなければならない。
Conditional Typesは強力だが、乱用するとTypeScriptコンパイラ(tsserver)のパフォーマンスを致命的に破壊する。
1. 深度の深い再帰型(Deeply Recursive Conditional Types)の爆発
型レベルでJSONパーサーや複雑なオブジェクトの深層を書き換えようとして、無限再帰や20階層を超える条件分岐を記述すると、TypeScriptコンパイラの型チェッカーがスタックオーバーフローを起こすか、CPU使用率が100%に張り付いてIDEがフリーズする。
TypeScript 4.5以降では再帰の制限緩和(Recursion Limit)が入ったが、それでも「動くからといって何でも型でやらない」という節度がエンジニアには求められる。
2. `infer` の乱用とユニオンの肥大化
`infer` キーワードを用いた型推論は強力だが、ユニオン型に対して分配条件付き型の中で `infer` を多用すると、コンパイラの計算量が爆発的に増加する(Combinatorial explosion)。
複雑な型を構築する際は、中間型(Utility Typeとして切り出す)を挟み、コンパイラのキャッシュ効率を意識した設計にすべきだ。
—
4. テックリードからの総括
プログラミング言語の進化は、「人間が手動で担保しなければならない規約」を「コンパイラが自動で検知するルール」へと昇華させる歴史だ。
Conditional Typesは、単なる小手先のテクニックではない。ビジネスロジックの制約、APIの契約、コンポーネントの状態遷移――これらすべてを型空間にマッピングし、「バグを書く自由」を奪い去るための最強の武器である。
次にコードを書くとき、あるいはコードレビューをするときに自問してほしい。
「このバリデーション、ランタイムに頼る前に、Conditional Typesで型レベルに落とし込めないか?」
その探求の先にこそ、真に堅牢なプロダクションコードが待っている。さあ、IDEを開き、型に命を吹き込め。