こんにちは!TypeScriptの世界へようこそ。
フロントエンドからバックエンドまで、型安全な開発の心地よさを知ってしまうと、もう他の言語には戻れなくなりますよね。
さて、今回はTypeScriptの型システムにおける「最強の魔法」の一つ、条件付き型(Conditional Types)についてお話しします。
「`T extends U ? X : Y` ってなんだか難しそう…」
そう思っていませんか?大丈夫です。ここをクリアすれば、あなたのTypeScriptスキルは一気に一段階上のステージへと駆け上がりますよ。
今日は、コンパイラの頭の中で何が起きているのかを覗き見しながら、この条件付き型の基礎を優しく、そして本質的にマスターしていきましょう!
—
1. 条件付き型(Conditional Types)ってなに?
一言で言うと、条件付き型とは「型のための三項演算子」です。
JavaScriptや他のプログラミング言語で、`condition ? trueの場合 : falseの場合` という書き方を見たことがありますよね。あれの「型バージョン」だと思ってください。
基本的な構文はこうなります。
T extends U ? X : Y
これを日本語に翻訳すると、こうなります。
「もし型 `T` が型 `U` に代入可能(extends)なら、結果は型 `X`。そうでなければ型 `Y` にしなさい」
これだけ聞くと「ふーん、で?」と思うかもしれませんが、これが型推論エンジンと組み合わさると、入力された型に応じて変幻自在に形を変える「動的な型」を作り出すことができるんです。
—
2. 脳内イメージ:コンパイラは型をどう判定している?
TypeScriptのコンパイラは、コードをビルド(コンパイル)するときに、コードの中の「型」が満たす条件を次々と評価していきます。
イメージとしては、こんな感じです。
[入力された型 T]
↓
「T は U の条件を満たしているか?」
┣━ Yes ──> [型 X を採用!]
┗━ No ──> [型 Y を採用!]
最もシンプルな具体例を見てみましょう。传入れた型が「文字列(string)」かどうかを判定する型を作ってみます。
// IsString 型の定義
type IsString
// 実際に使ってみる(型を代入して評価)
type Test1 = IsString
type Test2 = IsString
コンパイラは、`Test1` を見たときに `string extends string` が「真(True)」だと判定し、結果として `”Yes, it is a string!”` というリテラル型を割り当てます。`number` の場合は「偽(False)」なので、後半の型が選ばれます。
どうですか?仕組み自体はとってもシンプルですよね。
—
3. 実践!関数と組み合わせた「スマートな型変化」
基礎がわかったところで、もう少し実践的な例を見てみましょう。
「受け取った引数の型によって、返り値の型を自動で切り替える関数」を作ってみます。
例えば、「もしID(数値)が渡されたらユーザー名を返し、名前(文字列)が渡されたら詳細オブジェクトを返す」ような関数を考えてみましょう。
// 条件付き型を使って、入力に応じた戻り値を定義する
type ApiResponse
? { userId: T; name: string }
: { query: T; results: string[] };
// この型を使って関数をオーバーロード(または型注釈)する
function fetcher
// 実装はダミーです
if (typeof input === “number”) {
return { userId: input, name: “Taro” } as ApiResponse
} else {
return { query: input, results: [“Apple”, “Banana”] } as ApiResponse
}
}
// — 使う側の体験 —
const res1 = fetcher(100);
// res1 の型は自動的に { userId: number; name: string; } に推論される!
const res2 = fetcher(“fruit”);
// res2 の型は自動的に { query: string; results: string[]; } に推論される!
お気づきでしょうか? 使う側は複雑な型を意識する必要がなく、ただ値を渡すだけで、TypeScriptの型推論エンジンが完璧に型を追跡してくれます。これが条件付き型の真骨頂です。
—
4. 陥りやすい罠と文法エラー:初心者がハマるポイント
ここで、初学者がよくやってしまう「つまずきポイント」をいくつかご紹介しておきます。知っておくだけで無駄なバグやエラーを防げますよ!
罠①: `extends` の左側は「何でもいいわけではない」
条件付き型の `T extends U` における `T` は、多くの場合ジェネリクス(型引数)として渡されてきます。具体的な型が決まっていない状態(抽象的な型)に対して条件を評価するためです。
もし、最初から具体的な型を書いてしまうと、条件分岐の意味がなくなってしまいます。
// ❌ 意味のない書き方(常に string になってしまう)
type BadExample = string extends number ? true : false;
罠②: 分配条件付き型(Distributive Conditional Types)の挙動に注意
ここが少しアドバンスドですが、超重要です。
ジェネリクス型 `T` にユニオン型(`A | B` のような「or」の型)を渡すと、条件付き型は自動的に「分配(バラバラに処理)」されます。
type ToArray
// string | number を渡すとどうなる?
type Result = ToArray
// 予想: (string | number)[] ?
// 実際: string[] | number[] (それぞれの型にバラされてから配列化される!)
「ユニオン型を渡したら、勝手に一つずつバラして評価される」という挙動(分配法則)があることを、頭の片隅に置いておいてください。これを防ぎたい場合は、次のように `T` を角括弧 `[]` で囲んでタプルにします。
// 分配を阻止するテクニック
type ToArrayStrict
type ResultStrict = ToArrayStrict
// 結果: (string | number)[] (ひとまとまりとして評価される)
—
まとめ:ここをクリアすれば基本はバッチリ!
お疲れ様でした!今回は、TypeScriptの条件付き型(`T extends U ? X : Y`)の基礎について解説しました。
- 条件付き型とは?:型のための三項演算子であり、入力に応じて動的に型を変化させる仕組み。
- どう動く?:コンパイラが `extends` の条件を評価し、真か偽かに応じて適切な型を採用する。
- 注意点:ユニオン型を渡したときの「分配法則」に気をつける。
条件付き型は、TypeScriptが提供する標準ライブラリ(`Omit` や `Extract`、`NonNullable` など)の内部でも大量に使われている、いわば「モダンTypeScriptの共通言語」です。
ここをマスターしたあなたなら、既存の型ユーティリティの仕組みもすんなり理解できるはず。ぜひご自身のコードでも試して、型を自在に操る楽しさを体感してみてくださいね!