【実務・中級編】Conditional Typesで実現する「型レベルの条件分岐」 – TypeScript コア・型システムの基礎解析バイブル

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 = T extends any ? T[] : never;

// 以下の評価はどうなるか?
type Result = ToArray;
// 答え: string[] | number[] (配列のユニオン)
// ×: (string | number)[] ではない!

ジェネリック型パラメータが裸(naked)の型として条件の左側にある場合、TypeScriptは自動的にユニオンを分解し、要素ごとに条件を適用してから再度ユニオンとして再構築する。
これが「分配条件付き型」だ。この挙動を意図的に殺したい(ユニオンをそのまま一塊として評価したい)場合は、以下のようにタプルでラップする。

type ToArrayNonDistributive = [T] extends [any] ? T[] : never;

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 extends void
? []
: [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を開き、型に命を吹き込め。

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