やあ。今日も今日とて型定義の海に溺れてるかい?
中級への階段を登り始めた君なら、そろそろ「なんとなく動く型」から「意図を完全にコンパイラに伝える型」への脱皮を図りたい頃合いだと思う。`string`や`number`、あるいは`unknown`を駆使した静的な型定義はもうマスターしたはずだ。
だが、現場のコードベースはそんなに甘くない。「引数に渡す型によって、返却される型を動的に変えたい」「ある設定フラグが`true`の時だけ必須になるプロパティを制御したい」——そんな、ランダムに揺らぐ要件に立ち向かうとき、君の武器となるのが今回解説する Conditional Types(条件付き型) だ。
今日は、公式ドキュメントの薄っぺらい解説の先にある、実務の現場で泥臭く使われているリアルな知見を共有しよう。
—
1. 条件付き型(Conditional Types)の基本構造と「裏側の挙動」
まずは基本のおさらいだ。 syntaxはこうだったな。
T extends U ? X : Y
直感的に言えば、JavaScriptの三項演算子の型バージョンだ。「もし型 `T` が型 `U` に代入可能(extends)ならば型 `X`、そうでなければ型 `Y`」になる。
ここで一つ、TypeScriptのコンパイラ(tsc)が裏側でどう動いているか、少し深掘りしておこう。
ブラウザやNode.js上で動くJavaScriptのランタイムには、実はこの条件付き型は1バイトたりとも存在しない。これらはすべてコンパイル時にTypeScriptの型チェッカー(TS Compiler)によって完全に評価され、ビルド成果物(JavaScript)を出力する際には跡形もなく消え去る。
コンパイラは、AST(抽象構文木)を走査する中でこの `T extends U ? X : Y` にぶつかると、ジェネリック型 `T` が具体的に何にバインドされるかを解決し、静的に型を「枝分かれ」させる。つまり、これはランタイムのパフォーマンスを一切汚染しない、完全なゼロコストのメタプログラミングなのだ。
—
2. 現場で即効性のある実践コード
百聞は一見にしかず。実務でよく遭遇する「APIレスポンスの型分岐」を例に取ろう。
エンドポイントから返ってくるデータ構造が、特定のモード(`”json”` または `”text”`)によって変わるシチュエーションだ。
そのままエディタにコピペして動かせるように書いた。じっくり見てくれ。
/
- APIのレスポンスモードを定義
/
type ResponseMode = “json” | “text”;
/
- モードに応じた返却値の型を決定する条件付き型
- T extends “json” ならパース済みのオブジェクト(ここでは例として Record
)、 - それ以外(”text”)なら生文字列を返す。
/
type ApiResponse
? { data: Record
: { data: string; status: number };
/
- 実務でよくある汎用的なフェッチ風ラッパー関数
- オーバーロードと条件付き型を組み合わせることで、呼び出し側の型安全性を極限まで高める
/
function fetcher
// ※実際の通信処理は省略し、モックを返す想定
return fetch(url).then(async (res) => {
const status = res.status;
if (mode === “json”) {
const data = await res.json();
return { data, status } as ApiResponse
} else {
const data = await res.text();
return { data, status } as ApiResponse
}
});
}
// — 使用例(型推論がどう効くか)—
// 1. jsonモードの場合
// resJson の型は Promise<{ data: Record
const resJson = fetcher(“/api/user”, “json”);
// 2. textモードの場合
// resText の型は Promise<{ data: string; status: number; }> に自動で確定する!
const resText = fetcher(“/api/logs”, “text”);
どうだ? 関数の呼び出し側でわざわざジェネリクスを明示しなくても、第二引数の文字列リテラル(`”json”` や `”text”`)を推論して、TypeScriptが勝手に返り値の型を切り替えてくれる。これが条件付き型の真骨頂だ。
—
3. 中級者が必ずハマる「罠」:分散条件付き型(Distributive Conditional Types)
さて、ここからが今日一番伝えたかった重要な話だ。
条件付き型を使う上で、避けて通れない「仕様の罠」がある。それが 分散条件付き型(Distributive Conditional Types) だ。
ジェネリック型 `T` に ユニオン型(例: `A | B`) を渡したとき、TypeScriptは自動的にそのユニオンをバラバラに分解し、一つひとつの型に対して条件付き型を適用したあと、再度ユニオンとして結合する。
言葉だけだと呪文のようだな。コードで見てみよう。
// T がユニオン型の場合の挙動
type ToArray
// “a” | “b” というユニオン型をぶち込んでみる
type Result = ToArray
// 予想:(string | number)[]
// 実際の答え:string[] | number[]
「あれ? 配列のユニオンじゃなくて、ユニオンの配列になってほしいんだけど……」と思ったはずだ。これが分散のせいで起こる現象だ。
分散を「防ぐ」テクニック
もし、ユニオンをバラバラに分解したくない、つまり「ユニオン型全体をひとつの塊として評価してほしい」ときはどうすればいいか?
答えは簡単で、`extends` の両辺をブラケット(`[]`)で囲んで、型推論コンテキストから外す(タプルにする) だけだ。
// 左右を [] で囲むことで、分散を堰き止める
type ToArrayNonDistributive
// 今度は期待通りに動く
type ResultSafe = ToArrayNonDistributive
// 答え:(string | number)[]
実務でユーティリティ型(例えば、複数のコンポーネントPropsのユニオンから特定のプロパティを安全に抜き出したい時など)を自作するとき、この「分散するか・しないか」をコントロールできないと、意図しない型崩壊を起こして夜中にバグを踏むことになる。ここは絶対に覚えておいてほしい。
—
4. プロのTips:`infer` キーワードとの組み合わせで世界が変わる
条件付き型を語る上で外せないのが、`infer` キーワードだ。
`T extends … ? … : …` の条件部の中で、部分的な型を「推論(キャプチャ)」して変数のように扱うことができる。
例えば、Promiseの解決値の型(Awaited)や、関数の戻り値の型をごっそり抜き出すユーティリティは、すべてこの `infer` の応用で作られている。
/
- 関数から「戻り値の型」だけをエレガントに抽出する独自ユーティリティ
/
type MyReturnType
// 使用例
function getUser() {
return { id: 1, name: “Taro Yamada”, role: “admin” as const };
}
// TypeIs = { id: number; name: string; role: “admin”; }
type TypeIs = MyReturnType
このように、条件付き型は単なる「IF文」ではなく、型パターンのマッチングと抽出を行う超強力なパーサーとして機能する。
—
まとめ
最後に、今日話した内容をギュッと要約しよう。
1. 条件付き型 (`T extends U ? X : Y`) はゼロコストのメタプログラミング:ランタイムには一切影響せず、コンパイル時のみ安全性を担保する。
2. 関数のオーバーロードや引数推論と組み合わせることで、呼び出し側に優しい、ガチガチに型安全なAPIを設計できる。
3. ユニオン型を渡すと自動で分散するという仕様(Distributive Conditional Types)に注意し、必要に応じて `[T] extends [U]` で防壁を作れ。
4. `infer` を組み合わせることで、既存の型から必要なパーツを自在にくり抜くことができる。
型定義に凝りすぎて可読性を落とすのは本末転倒だが、今日紹介したような条件付き型の引き出しを持っていれば、複雑なフロントエンドのドメインロジックやサードパーティライブラリの型定義にも、怯むことなく立ち向かえるはずだ。
さて、コーヒーブレイクはここまでだ。エディタに戻って、そのスマートな型を君のコードベースに組み込んでやろうぜ。

コメント