【実務・中級編】 Conditional Types (条件付き型) – TypeScript実践ガイド

やあ。今日も今日とて型定義の海に溺れてるかい?

中級への階段を登り始めた君なら、そろそろ「なんとなく動く型」から「意図を完全にコンパイラに伝える型」への脱皮を図りたい頃合いだと思う。`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 = T extends “json”
? { data: Record; status: number }
: { data: string; status: number };

/

  • 実務でよくある汎用的なフェッチ風ラッパー関数
  • オーバーロードと条件付き型を組み合わせることで、呼び出し側の型安全性を極限まで高める

/
function fetcher(url: string, mode: T): Promise> {
// ※実際の通信処理は省略し、モックを返す想定
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; status: number; }> に自動で確定する!
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 = T extends any ? T[] : never;

// “a” | “b” というユニオン型をぶち込んでみる
type Result = ToArray;
// 予想:(string | number)[]
// 実際の答え:string[] | number[]

「あれ? 配列のユニオンじゃなくて、ユニオンの配列になってほしいんだけど……」と思ったはずだ。これが分散のせいで起こる現象だ。

分散を「防ぐ」テクニック

もし、ユニオンをバラバラに分解したくない、つまり「ユニオン型全体をひとつの塊として評価してほしい」ときはどうすればいいか?
答えは簡単で、`extends` の両辺をブラケット(`[]`)で囲んで、型推論コンテキストから外す(タプルにする) だけだ。

// 左右を [] で囲むことで、分散を堰き止める
type ToArrayNonDistributive = [T] extends [any] ? T[] : never;

// 今度は期待通りに動く
type ResultSafe = ToArrayNonDistributive;
// 答え:(string | number)[]

実務でユーティリティ型(例えば、複数のコンポーネントPropsのユニオンから特定のプロパティを安全に抜き出したい時など)を自作するとき、この「分散するか・しないか」をコントロールできないと、意図しない型崩壊を起こして夜中にバグを踏むことになる。ここは絶対に覚えておいてほしい。

—

4. プロのTips:`infer` キーワードとの組み合わせで世界が変わる

条件付き型を語る上で外せないのが、`infer` キーワードだ。
`T extends … ? … : …` の条件部の中で、部分的な型を「推論(キャプチャ)」して変数のように扱うことができる。

例えば、Promiseの解決値の型(Awaited)や、関数の戻り値の型をごっそり抜き出すユーティリティは、すべてこの `infer` の応用で作られている。

/

  • 関数から「戻り値の型」だけをエレガントに抽出する独自ユーティリティ

/
type MyReturnType = T extends (…args: any[]) => infer R ? R : never;

// 使用例
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` を組み合わせることで、既存の型から必要なパーツを自在にくり抜くことができる。

型定義に凝りすぎて可読性を落とすのは本末転倒だが、今日紹介したような条件付き型の引き出しを持っていれば、複雑なフロントエンドのドメインロジックやサードパーティライブラリの型定義にも、怯むことなく立ち向かえるはずだ。

さて、コーヒーブレイクはここまでだ。エディタに戻って、そのスマートな型を君のコードベースに組み込んでやろうぜ。

コメント

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