おい、最近型のパズルにハマりすぎて、実際のコンポーネント設計そっちのけになってないか?
……気持ちはよく分かる。TypeScriptの型システムって、一度沼にハマると抜け出せない魔力があるよな。特に「Conditional Types(条件付き型)」をマスターした瞬間、自分がまるで型世界の魔法使いになったような全能感を覚えるはずだ。
でもな、現場で求められるのは「動く、かつ保守しやすい、他のメンバーが絶望しない型定義」だ。自己満足の複雑怪奇な型パズルは、コードレビューで容赦なく「何これ?」って差し戻されるのがオチだからな。
今日は、そんなConditional Typesの基本構造 `T extends U ? X : Y` について、実務で本当に使えるレベルまで解像度を上げて解説してやる。気合入れてついてこいよ。
—
1. Conditional Typesってそもそも何だっけ?(基礎の確認)
一言で言えば、「型レベルの三項演算子」だ。
JavaScriptやTypeScriptのランタイムコードで `condition ? trueVal : falseVal` って書くだろう?あれの型版だよ。
基本的なシンタックスはこうだ。
T extends U ? X : Y
日本語に翻訳するぜ。「もし型 `T` が型 `U` に代入可能(extends)ならば、結果の型は `X`。そうでなければ `Y`」だ。
「なんだ、ただのif文か」と思ったそこのお前。甘い。この `extends` というキーワード、クラスの継承だけじゃなくて、「型の包含関係(部分型かどうか)」を判定するウルトラ強力な演算子なんだ。ここを勘違いしてると、この先で確実に頭を抱えることになるから覚えておけ。
—
2. ブラウザの裏側で何が起きているのか?(TSコンパイラの視点)
ここで一つ、シニアとしてお前らに知っておいてほしい「裏側の話」をしておこう。
「ブラウザがどう処理するか?」という問いに対して正確に言うと、ブラウザ上で動くJavaScriptのランタイムは、Conditional Typesの存在すら知らない。
TypeScriptの型システムは、あくまでtsc(TypeScriptコンパイラ)やエディタ(VSCodeのLanguage Server)の内部で動く静的なチェッカーだ。
コードがビルド(トランスパイル)されるとき、これらすべての複雑な型定義は跡形もなく消し去られ、ただのプレーンなJavaScriptになる。
コンパイラ内部では、AST(抽象構文木)から型チェックのフェーズに入った際、この `T extends U ? X : Y` に遭遇すると、次のようなステップで評価を行っている:
1. 型引数の具現化(Instantiation): ジェネリック型に具体的な型が渡された時、コンパイラはその実体を評価する。
2. 割当可能性の判定(Assignability Check): `T` が `U` の要件を満たしているかをメモリ上で判定する。
3. 型の置換(Substitution): 条件が真なら `X` に、偽なら `Y` に一瞬で型を置き換える。
つまり、型定義の中でどれだけ複雑な条件分岐をしようとも、ランタイムのパフォーマンスには1ミリも影響しない。だからこそ、俺たちは安心して型をこねくり回せるわけだ。ただし、エディタの補完速度(型推論の重さ)には直結するから、無限ループみたいな地獄の型定義は絶対にご法度だぞ。
—
3. 現場で即コピペして使える!実用的なサンプルコード
理屈はこの辺にして、実務でどう使うかを見せよう。
APIから返ってくるレスポンスの型をハンドリングする、よくあるシチュエーションを想定してくれ。
以下のコードをそのままエディタにコピペすれば、挙動がバッチリ確認できるはずだ。
/
- 【実務Tips】
- 引数の型によって、返却するデータの構造を動的に切り替えるカスタムフックやユーティリティを想定した例。
/
// 1. サーバーからのレスポンス型定義のモック
type UserResponse = { id: string; name: string; type: ‘user’ };
type AdminResponse = { id: string; permissions: string[]; type: ‘admin’ };
type GuestResponse = { id: string; type: ‘guest’ };
type ApiResponse = UserResponse | AdminResponse | GuestResponse;
// 2. Conditional Typesを使った動的な型抽出
// T が ‘admin’ を含むかどうかに応じて、対応するレスポンス型を厳密に絞り込む
type ExtractResponse
? AdminResponse
: T extends ‘user’
? UserResponse
: GuestResponse;
// — 動作確認用の関数 —
/
- ユーザーの種別に応じて、適切な権限やデータを安全に処理する関数
/
function fetchDashboardData
userType: T
): ExtractResponse
// 実装のモック(実際はここでAPI叩いたりする)
const mockData = {
id: ‘123’,
name: ‘Taro’,
permissions: [‘read’, ‘write’],
type: userType,
};
// 返り値の型が Conditional Types によって完全に保証されているため、
// 呼び出し元の型安全性が爆上がりする。
return mockData as ExtractResponse
}
// — 利用側のコード(IDEの補完と型チェックを確認してみてくれ)—
// 変数 adminData の型は自動的に「AdminResponse」に決定される!
const adminData = fetchDashboardData(‘admin’);
// adminData.permissions アクセスは成功するが、name は存在しないのでコンパイルエラーになる
console.log(adminData.permissions);
// 変数 guestData の型は自動的に「GuestResponse」に決定される!
const guestData = fetchDashboardData(‘guest’);
// guestData.permissions や guestData.name は存在しないのでエラーになる
どうだ?この例では、引数に渡したリテラル型(`’admin’` など)をフックにして、条件分岐(Conditional Types)で戻り値の型をガラリと変えている。
もしこれを知らなかったら、全部の型をUnionで混ぜて `any` や `Omit` だらけの汚いコードになっていただろうな。
—
4. シニアからのお節介:初心者が踏みがちな「罠」
最後に、Conditional Typesを使う上で、多くのエンジニアが一度はハマる「分散条件付き型(Distributive Conditional Types)」というトラップについて軽く触れておく。
型引数 `T` が Union型(例: `string | number`) のとき、Conditional Typesは自動的に各要素にバラされて(分散されて)評価される。
type ToArray
// これをやると、string[] | number[] になる(string[] と number[] のunion)
type Result = ToArray
「あれ、全体をひとつの `T` として判定してほしかったのに、勝手にバラバラになっちゃったよ……」という時は、`extends` の両辺をブラケット `[]` で囲ってやると分散を抑止できる。
type ToArrayNonDistributive
// これだと [string | number][] という単一の配列の型になる
type Result2 = ToArrayNonDistributive
この挙動の違い、実務で独自のユーティリティ型(Utility Types)を自作し始めると99%の確率でブチ当たる壁だから、今のうちに頭の片隅に入れておきな。
—
まとめ
- Conditional Types は `T extends U ? X : Y` で書く「型レベルの三項演算子」。
- ランタイムには一切影響せず、コンパイル時の静的解析と型推論を強力にサポートする。
- 実務では、APIレスポンスの切り替えや、入力値に応じた戻り値の厳密な制御に使うと、コードの安全性が劇的に向上する。
型定義を極めるのは面白い。でも、一番大事なのは「チームのメンバーが読めて、メンテナンスしやすいコードであること」だ。変に複雑にしすぎず、適切な粒度でこの強力な武器を使いこなしてくれ。
それじゃ、今日のレビューに戻るとしようか。お疲れ!

コメント