【入門編】関数型における「Conditional Types」を用いた戻り値の動的決定 – TypeScript コア・型システムの基礎解析バイブル

こんにちは。TypeScriptの世界へようこそ。

型システムという強力な武器を手にすると、これまで「実行してみないと分からない」と不安に思っていたバグが、書いている最中に次々と浮き彫りになる感覚を味わえるようになります。

今回は、TypeScriptの真骨頂とも言える「Conditional Types(条件付き型)」を用いて、引数の型によって戻り値を動的に変化させる魔法のような関数を作ってみましょう。ここを理解すれば、あなたのコードは劇的に堅牢で、かつ柔軟なものへと進化します。

—

なぜ戻り値を「動的」にする必要があるのか?

例えば、ある関数に「文字列を渡せば文字列を返し、数値を渡せば数値を返す」という仕様を作りたいとします。単純なオーバーロードでも実現可能ですが、Conditional Typesを使うと、型定義をよりスマートに、かつ型安全にカプセル化できます。

まずは、以下のコードを見てください。

// T というジェネリクス型を定義
// T が string なら string を、そうでなければ number を返すという条件式
type StringOrNumber = T extends string ? string : number;

function processValue(value: T): StringOrNumber {
// 実際の実装ではキャストが必要になるケースが多いですが、
// 型システム上はこれで「引数に応じた戻り値」が保証されます
return value as any;
}

const result1 = processValue(“TypeScript”); // result1 は string 型
const result2 = processValue(100); // result2 は number 型

このコードの「裏側」で何が起きているのか?

TypeScriptのコンパイラは、`processValue`が呼ばれた瞬間、`T`に代入された型をスキャンします。「あ、今回は`string`が渡されたな。じゃあ条件式`T extends string`は真(true)だから、戻り値の型は`string`に決定だ!」と判断しているのです。

これがコンパイル時に行われる「型レベルのプログラミング」です。実行時のif文とは異なり、コンパイル時点で型が確定するため、実行速度に影響を与えることは一切ありません。

—

現場でよく使う「実用パターン」

もう少し踏み込んで、APIのレスポンス処理などでよくある「IDを渡すとオブジェクトを、そうでなければリストを返す」ような関数を考えてみましょう。

interface User { id: number; name: string; }

// T が number かどうかで戻り値を変える
type ApiResponse = T extends number ? User : User[];

function fetchUserData(query: T): ApiResponse {
if (typeof query === ‘number’) {
// IDが渡された場合、単一のユーザーを返す
return { id: query, name: “Taro” } as any;
} else {
// それ以外(何も渡さない等)はユーザー一覧を返す
return [{ id: 1, name: “Taro” }] as any;
}
}

const user = fetchUserData(1); // user は User 型
const users = fetchUserData(null); // users は User[] 型

—

初学者が陥りやすい「3つの罠」

Conditional Typesは強力ですが、慣れないうちは以下のポイントで躓きがちです。

1. `as any` や `as unknown` の多用

TypeScriptの型ガード(`typeof`や`instanceof`)を使っても、複雑なConditional Typesをコンパイラが完全に追跡できない場合があります。無理に型を合わせようとして`as any`を連発すると、型安全性が崩壊します。「型推論が効かない場所だけを必要最小限にキャストする」のがプロの流儀です。

2. 「分配法則(Distributive Conditional Types)」の誤解

ユニオン型(`string | number`)をジェネリクスとして渡すと、TypeScriptは自動的に型を分解して条件を適用します。

type Result = string | number extends string ? “A” : “B”;
// これが、”A” | “B” になるのか “B” になるのか混乱しがちですが、
// 正解は「分配される」ため、両方の判定結果がマージされます。

この「勝手に分解される」挙動を知らないと、思わぬ型が返ってきて混乱することがあります。

3. 無理な複雑化

Conditional Typesを入れ子にしすぎると、TypeScriptの型チェック速度(コンパイル時間)が低下します。可読性とパフォーマンスのバランスを常に意識してください。「複雑な型定義を書くよりも、関数を分けたほうが保守しやすい」という判断も、熟練エンジニアの重要なスキルです。

—

まとめ:ここをクリアすれば、あなたはもう中級者

Conditional Typesをマスターするということは、「値の型をコンパイラに計算させる」というTypeScriptの核心に触れるということです。

最初は難しく感じるかもしれませんが、まずは「引数の型によって戻り値を出し分ける」というシンプルなパターンから使い始めてみてください。それが当たり前に書けるようになれば、あなたのコードは「型安全な要塞」へと変わります。

もし途中でつまずいたら、いつでも戻ってきてください。TypeScriptの旅はまだ始まったばかりです。一緒に、型という名の地図を広げていきましょう。

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