皆さん、こんにちは! TypeScriptの世界へようこそ。最高のフロントエンド体験を追求する旅路で、皆さんとご一緒できることを心から嬉しく思います。チーフアーキテクトの私が、今日は皆さんが「なるほど!」と膝を打つような、TypeScriptの奥深さと、その使いこなしかたを、優しく、そして泥臭くも実践的な視点でお話ししましょう。
今日のテーマは、TypeScriptの型定義の中でも、特に柔軟で強力な機能の一つ、「Conditional Types(条件型)」と、その心臓部である「`extends`」キーワードについてです。
「条件型? `extends`? なんか難しそう……」と思ったあなた、大丈夫ですよ。私たちはプロフェッショナルですから、どんなに複雑な概念も、身近な例え話と、現場で培った知見で、スッキリと理解できるように紐解いていきます。
さあ、一緒にTypeScriptの真髄に触れていきましょう!
—
「もしも〜なら、この型。そうでなければ、あの型」:柔軟な型定義への扉
TypeScriptを使っていると、こんな風に思ったことはありませんか?
「もし、この変数の型が `string` だったら、戻り値は `string` にしたいな。でも、もし `number` だったら、戻り値は `number` にしたい!」
あるいは、「このオブジェクトに特定のプロパティがあるかないかで、別のプロパティの型を変えたいな」なんてことも。
これ、まさに「条件分岐」ですよね。普段、JavaScriptで `if/else` を使ってロジックを分岐させるように、TypeScriptの世界でも「型」を条件によって分岐させることができたら、どんなに便利でしょう?
はい、それができるんです! それがConditional Types(条件型)と呼ばれる機能です。そして、その条件を判定するために使われるのが、今日の主役である「`extends`」キーワードなんですね。
`extends` って何だろう? 身近なもので例えてみよう
多くのプログラミング言語で `extends` というと、「継承」や「拡張」という意味で使われることが多いですよね。例えば、「猫は動物を継承する」のように。
でも、TypeScriptのConditional Typesにおける `extends` は、ちょっとニュアンスが違うんです。ここでは、むしろ「〜であるか?」とか「〜の仲間か?」と尋ねるようなイメージを持ってもらうと、グッと理解しやすくなります。
身近な例で考えてみましょう。
例え話:鍵と鍵穴の物語
あなたの手元に、いくつかの種類の鍵があるとします。そして、目の前には一つだけ鍵穴があります。
- 「この『細い鍵』は、この『鍵穴』に合うかな?」
- 「この『太い鍵』は、この『鍵穴』に合うかな?」
鍵が鍵穴に「合う」かどうかは、鍵穴が求める「形」や「サイズ」といった要件を、鍵がすべて満たしているかで決まりますよね。
TypeScriptの `extends` も、まさにこの「鍵が鍵穴に合うか」をチェックするようなものなんです。
`TypeA extends TypeB` と書いた時、それは「`TypeA` は、`TypeB` のすべての要件を満たしているか?」と尋ねているのと同じです。
Conditional Typesの基本形と `extends` の役割
Conditional Typesは、三項演算子(`条件 ? 真の場合 : 偽の場合`)のような構文で書かれます。
type ResultType
これを先ほどの鍵の例で当てはめてみると、こうなります。
// 型引数 T は、チェックしたい「鍵」
// 型 U は、チェックしたい「鍵穴の要件」
type CanOpenDoor
この `T extends U` の部分が、まさに「鍵穴に合うか?」を判定しているところなんですね。
- もし `T` が `U` の要件をすべて満たしていれば(`true` と判定されれば)、`ResultType` は `TrueType` になります。
- そうでなければ(`false` と判定されれば)、`ResultType` は `FalseType` になります。
「適合する」ってどういうこと? 型の互換性を深掘り!
ここが一番のポイントであり、初学者が少しつまずきやすいところかもしれません。でも、心配いりません。一つずつ、じっくりと見ていきましょう。
TypeScriptにおける「適合する」(`extends` が `true` になる)とは、ざっくり言うと「左側の型が、右側の型でできることを、すべてこなせるか?」という意味です。
別の言い方をすれば、「左側の型が、右側の型よりも、より具体的であるか、あるいは同じ型であるか」というイメージです。
例え話で考えてみましょう。
例え話:お買い物リストと持っている商品
あなたがスーパーでお買い物をする場面を想像してください。
1. お買い物リスト: 「果物」
- 持っている商品: 「リンゴ」
- 「リンゴ」は「果物」のリストに適合しますか? → はい、適合します。 リンゴは果物の一種だから、リストの要件(果物であること)を満たしていますね。
- `’りんご’ extends ‘果物’` ではないですが、`Apple extends Fruit` のようなイメージです。
2. お買い物リスト: 「リンゴ」
- 持っている商品: 「果物」(なんでもいいから果物を持ってます)
- 「果物」は「リンゴ」のリストに適合しますか? → いいえ、適合しません。 リストは「リンゴ」という具体的なものを求めているのに、「果物」ではリンゴかもしれないし、バナナかもしれないし、イチゴかもしれない。リストの要件(リンゴであること)を確実に満たしているとは言えないからです。
これがTypeScriptの `extends` の感覚に非常に近いです。
「左側の型は、右側の型が要求する全ての特性を持っているか?」という問いかけです。
実際の型で見てみよう!
type Result1 = string extends string ? true : false;
// string は string の要件を全て満たします(同じ型なので)
// -> true
type Result2 = ‘hello’ extends string ? true : false;
// ‘hello’ という具体的な文字列リテラル型は、string 型の要件を全て満たします
// (’hello’ は string の一種だから)
// -> true
type Result3 = string extends ‘hello’ ? true : false;
// string 型は ‘hello’ という特定の文字列リテラル型の要件を全て満たしますか?
// いいえ、満たしません。string 型は ‘hello’ だけでなく、’world’ や ‘TypeScript’ など、
// あらゆる文字列を含むので、必ずしも ‘hello’ であるとは限りません。
// -> false
type Result4 = number extends unknown ? true : false;
// number 型は unknown 型の要件を全て満たしますか?
// unknown は「何でもあり」の型なので、どんな型も unknown の要件を満たします。
// (どんな鍵でも、どんな鍵穴にも「適合する」と見なすようなもの)
// -> true
type Result5 = unknown extends number ? true : false;
// unknown 型は number 型の要件を全て満たしますか?
// いいえ、満たしません。unknown は number だけでなく、string や boolean など
// あらゆる型を含みうるので、必ずしも number であるとは限りません。
// -> false
type Result6 = { name: string, age: number } extends { name: string } ? true : false;
// 左の型は、右の型が求める「name: string」という要件を確かに持っています。
// さらに age プロパティも持っていますが、それは右の型が要求していないだけなので問題ありません。
// (「リンゴとバナナを持っている」は「リンゴが欲しい」というリストの要件を満たす)
// -> true
type Result7 = { name: string } extends { name: string, age: number } ? true : false;
// 左の型は、右の型が求める「age: number」という要件を持っていません。
// (「リンゴを持っている」は「リンゴとバナナが欲しい」というリストの要件を満たさない)
// -> false
どうでしょうか? `extends` が「右の型が求める要件を、左の型がすべて満たしているか?」というチェックであることが、少しずつ見えてきたでしょうか。この考え方をマスターすれば、もう大丈夫です!
実際にConditional Typesを使ってみよう!
それでは、この `extends` の知識を使って、便利なConditional Typesを定義してみましょう。
例1:型が特定のプリミティブ型か判定する
「もし型 `T` が `string` ならば `string` を返し、そうでなければ `never` を返す」という型を作ってみましょう。`never` は「決して発生しない型」という意味で、ここでは「該当しない」という印として使います。
type IsString
// string型を渡すと string になります
type A = IsString
// number型を渡すと never になります
type B = IsString
// boolean型を渡すと never になります
type C = IsString
// 複数の型を組み合わせたユニオン型の場合 (後述の「分散型条件型」)
type D = IsString
// これは string に絞り込まれた状態を表します
// JavaScriptのコードで型を確認してみましょう
function identifyType
// 実際には実行時の値で型を判定するのは難しいですが、
// TypeScriptの型システム上はこのように表現できます。
if (typeof value === ‘string’) {
return value as IsString
}
// stringでなければ never なので、ここでは何らかの型安全な方法で never を返す必要があります
// 例: エラーをthrowするか、実行時型ガードで型を絞り込むなど
throw new Error(“この値はstring型ではありません。”);
}
// 実際に使うとこんな感じ
const stringResult: string = identifyType(“Hello TypeScript”); // OK
// const numberResult: string = identifyType(123); // コンパイルエラー: string に never を割り当てられない
例2:オブジェクトのキーによって型を取り出す
オブジェクトの型から、特定のキーに対応するプロパティの型を取り出したい時にも使えます。もし指定したキーがオブジェクトに存在しない場合は、エラーを意味する `never` を返しましょう。
/
- オブジェクト型 `T` から、キー `K` に対応するプロパティの型を取り出します。
- もし `K` が `T` のキーでなければ `never` を返します。
/
type GetPropertyType
interface User {
id: number;
name: string;
email?: string; // オプショナルなプロパティ
}
// User型から ‘name’ プロパティの型を取り出す
type UserNameType = GetPropertyType
// User型から ‘id’ プロパティの型を取り出す
type UserIdType = GetPropertyType
// User型から存在しない ‘address’ プロパティの型を取り出す
type UserAddressType = GetPropertyType
// ここで K extends keyof T が重要!
// ‘name’ は keyof User (つまり ‘id’ | ‘name’ | ‘email’) に適合するので、T[‘name’] が選ばれる。
// ‘address’ は keyof User に適合しないので、never が選ばれる。
// 実際のコードでどう役立つか
function getPropertyValue
return obj[key];
}
const user: User = { id: 1, name: “Alice” };
const nameValue = getPropertyValue(user, ‘name’); // nameValue の型は string
const idValue = getPropertyValue(user, ‘id’); // idValue の型は number
// const emailValue = getPropertyValue(user, ‘email’); // emailValue の型は string | undefined
// const addressValue = getPropertyValue(user, ‘address’); // コンパイルエラー: ‘address’ は User 型のプロパティではない
このように、`extends` を使ったConditional Typesは、非常に柔軟で強力な型定義を可能にし、私たちのコードに堅牢な型安全性をもたらしてくれます。
ちょっとだけ高度な話:分散型条件型 (Distributive Conditional Types)
これは少しだけ高度な話題なので、「へー、そんなこともあるんだな」くらいに聞き流してもらっても大丈夫ですよ。
型引数 `T` が、`string | number` のようなユニオン型(複数の型を `|` で繋いだもの)の場合、Conditional Typesは、そのユニオン型の各メンバーに対して個別に `extends` のチェックを適用し、結果を再びユニオン型として返します。
これを「分散型条件型 (Distributive Conditional Types)」と呼びます。
type MyType
// T が string | number の場合、TypeScriptはこれを以下のように展開してくれます
// (string extends string ? ‘isString’ : ‘notString’) | (number extends string ? ‘isString’ : ‘notString’)
type ResultUnion = MyType
// ↑ 実際には ‘isString’ | ‘notString’ になります
// string の場合は ‘isString’
// number の場合は ‘notString’
// それらがユニオン型として結合される、というわけです。
type ResultBooleanOrNumber = MyType
// ↑ ‘notString’ | ‘notString’ → ‘notString’ になります
これは非常に強力な機能で、TypeScriptの組み込みユーティリティ型(`Exclude
例えば、`Exclude
// Exclude
type MyExclude
type MixedUnion = string | number | boolean;
type ExcludedStrings = MyExclude
// 以下のように展開されます:
// (string extends string ? never : string) |
// (number extends string ? never : number) |
// (boolean extends string ? never : boolean)
// ↓
// never | number | boolean
// ↓
// type ExcludedStrings = number | boolean
どうですか? `extends` が、型の世界でいかに柔軟な「判断」を可能にしているか、少しでも感じてもらえたら嬉しいです。
まとめ:`extends` は型の「適合性」を問い、TypeScriptに知性を与える
今日は、TypeScriptのConditional Typesの核心にある `extends` キーワードについて、じっくりと見てきました。
- `extends` は、「左側の型が、右側の型の要件をすべて満たしているか?」 をチェックする演算子です。これは、特定の鍵が鍵穴に合うか、あるいは持っている商品がお買い物リストの要件を満たすか、といった「適合性」を問うイメージです。
- この `extends` を使った条件判定 `T extends U ? TrueType : FalseType` が、Conditional Types(条件型)として、TypeScriptの型システムに「もしも〜なら、この型。そうでなければ、あの型」という柔軟な条件分岐をもたらします。
- オブジェクトのプロパティの有無や、ユニオン型からの型の絞り込みなど、様々な場面で `extends` が活用され、私たちのコードをより安全で、表現豊かなものにしてくれます。
初めは少し難しく感じるかもしれませんが、大丈夫です。TypeScriptの型システムは奥深く、一度に全てを理解する必要はありません。一つずつ、身近な例え話と実践的なコード例を通して、手を動かしながら慣れていくことが何よりも大切です。
今日の話が、皆さんのTypeScript学習の助けとなり、コードを書くのがもっと楽しくなってもらえたら、チーフアーキテクトとしてこれほど嬉しいことはありません。
これからも、TypeScriptの旅路を一緒に楽しみましょう!

コメント