TypeScriptのextendsを完全攻略|継承・制約・条件型の3用法
TypeScriptのextendsはコードのあちこちで出てくるのに、場面ごとに意味が変わって見えて混乱しがちですよね。
<T extends U> にも出てくる。同じ単語なのに別物なの?
T extends U ? X : Y みたいな書き方、? も : も何を意味してるのか分からない…
extendsは見た目こそ場面ごとに違いますが、実は「左の型が右の型を満たすか」という一つの考え方で全部つながっています。この記事では、継承・型引数の制約・条件型という3つの用法を1枚の早見表と判定の流れで整理し、どの場面のextendsでも迷わず読めるようにします。
この記事は次のような方におすすめです。
- クラス継承でしかextendsを見たことがなく、ジェネリクスの制約で戸惑っている方
T extends U ? X : Yの条件型が読めず、何が起きているか知りたい方- extendsとimplementsの違いをきちんと言葉にできない方
- 同じextendsが3つの意味を持つ理由を一度で腑に落としたい中級者の方
読み終えるころには、どの場面のextendsも「左が右を満たすか」という同じ視点で読み解けるようになり、制約や条件型のコードに出会っても落ち着いて意味を追えるようになります。
それでは、順を追って詳しく見ていきましょう!
- 未経験で後悔したくない
【実体験】未経験からITエンジニアに転職して後悔した話|4社経験してわかった「最初の選択ミス」 - 年収が低くて不安
4年間ずっと年収260万だったエンジニアが、転職で510万になるまでの全記録
extends とは何か|継承ではなく「左が右を満たすか」の一語
extendsを「継承」とだけ覚えていると、ジェネリクスや条件型で出てきたときに意味がつながらなくなります。TypeScriptのextendsは、文脈を問わず「左の型は右の型を満たすか(右の部分型か)」を表す一語だと捉えるのが、3用法を貫く近道です。継承も制約も条件型も、この「満たすかどうか」という一つの判定を別の場面に当てはめているにすぎません。
同じextendsが、次の3つの文脈でそれぞれ違う顔を見せます。
// ① クラス/interfaceの継承:左の型が右の型を満たす形で拡張する
class Dog extends Animal {}
// ② 型引数の制約:左の型を、右の型を満たすものだけに限定する
function f<T extends U>(x: T) {}
// ③ 条件型:左の型が右の型を満たすなら X、そうでなければ Y
type Result = T extends U ? X : Y;
書き方は違っても、共通しているのは「左の型が右の型の条件を満たしているか」という見方です。①は「満たす形で受け継ぐ」、②は「満たす型だけ受け付ける」、③は「満たすかで型を分岐する」と読み替えられます。この統一視点を持っておくと、初見の extends でも「これは何が何を満たすと言っているのか」と問うだけで意味が取れます。
extends の3用法を1枚で整理(早見表)
3つの用法は、構文の形を見れば見分けられます。extendsの左右が型か値か、そして後ろに ? が続くかを見るだけで、どの用法かが判定できます。
| 用法 | 構文 | 読み方(意味) | 典型用途 |
|---|---|---|---|
| ①クラス/interfaceの継承 | class B extends A / interface B extends A |
BはAを受け継いで拡張する | 既存の型・実装を引き継いで機能を足す |
| ②型引数の制約 | <T extends U> |
TはUを満たす型に限定する | ジェネリクスで扱える型を絞り安全にする |
| ③条件型 | T extends U ? X : Y |
TがUを満たすなら X、違えば Y | 型から別の型を導出・分岐する |
見分け方はシンプルです。
class/interfaceのあとに置かれていれば継承。- 型引数
<...>の中にあれば制約。 - 後ろに
? ... : ...が続けば条件型。
それぞれの詳しい挙動を、次から順番に見ていきます。
用法1:クラスとinterfaceの継承(extends)
継承のextendsは、継承元(親)が持つプロパティやメソッドを、継承先(子)がそのまま引き継いだうえで拡張できる仕組みです。クラスでもinterfaceでも、書いた瞬間に親のメンバーが子の型に含まれます。
class Animal {
move() {
console.log("移動する");
}
}
class Dog extends Animal {
bark() {
console.log("ワン");
}
}
const dog = new Dog();
dog.move(); // 親Animalから継承:移動する
dog.bark(); // Dog自身のメソッド:ワン
Dog は Animal を継承しているので、move() を定義していなくても呼び出せます。これがクラス継承の「実装ごと引き継ぐ」挙動です。
interfaceの継承も形は同じで、親の型に新しいプロパティを足して、より具体的な型へ絞り込めます。
interface Animal {
name: string;
}
interface Dog extends Animal {
bark(): void;
}
const dog: Dog = {
name: "ポチ", // Animalから継承したプロパティ
bark() {}, // Dogで追加したメソッド
};
Dog は Animal の name を引き継ぎつつ bark を追加しており、Dog は Animal の部分型(より具体的な型)になります。継承は「親を満たす形で広げる」操作だと捉えると、後の制約・条件型ともつながります。
継承とよく混同されるのがimplementsです。両者の違いは次の小さな表で整理できます。
| キーワード | やること | 親の実装 |
|---|---|---|
| class extends | 型と実装を受け継いで拡張する | 引き継ぐ |
| interface extends | 型メンバーを拡張する | 実装なし |
| implements | その型を満たしているか検査する | 引き継がない |
implements との違い(簡潔に)
extendsとimplementsは、似た位置に書くのに役割が正反対です。extendsは親の実装を受け継ぐのに対し、implementsは「その型を満たしているか」を検査するだけで実装は持ち込みません。
interface Walkable {
walk(): void;
}
class Robot implements Walkable {
walk() {} // Walkableを満たすため自分で実装する必要がある
}
implements Walkable は「Robot は Walkable の形を満たしているか」を型チェックさせる宣言で、walk の中身は自分で書かなければなりません。継承のように実装が降ってくるわけではない点が、両者の決定的な違いです。
用法2:型引数の制約(T extends U)
ジェネリクスで T extends U と書くと、型引数 T に入れられる型を「Uを満たす型」だけに限定できます。制約を付けない T は「あらゆる型かもしれない」ため、特定のプロパティへのアクセスが許されません。
function getId<T>(x: T) {
return x.id; // 例:TS2339 Property 'id' does not exist on type 'T'.(TypeScriptバージョンにより文言が異なる場合あり)
}
T が何の型か分からない以上、id を持つ保証がないためエラーになります。ここで T extends { id: number } という制約を足すと、「T は必ず id を持つ」とコンパイラに伝わり、安全にアクセスできます。
function getId<T extends { id: number }>(x: T) {
return x.id; // OK:Tは必ずidを持つと保証される
}
getId({ id: 1, name: "a" }); // OK
getId({ name: "a" }); // 例:TS2345 Argument of type '{ name: string; }' is not assignable to parameter of type '{ id: number; }'.(TypeScriptバージョンにより文言が異なる場合あり)
制約を満たさない値を渡すと、does not satisfy the constraint 系のエラーで弾かれます。制約は「Tの自由度を狭めて、扱える操作を増やす」トレードオフだと理解するのがポイントです。
なお、T = string のように = でデフォルト型引数を添えると、型引数を省略・推論できないときの既定の型を決められます。
function createList<T = string>(): T[] {
return [];
}
const strings = createList(); // 型引数を省略:T は既定の string → string[]
const numbers = createList<number>(); // 明示すれば number[]
createList() のように型引数を省略すると T は既定の string になり、createList<number>() と明示すればその型で上書きされます。
keyof と組み合わせる制約
制約は keyof と組み合わせると、より実用的になります。K extends keyof T と書けば、K を「T に実在するキーだけ」に限定でき、存在しないキーの指定をコンパイル時に防げます。
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "ポチ" };
getProp(user, "name"); // OK:戻り値は string
getProp(user, "age"); // 例:TS2345 Argument of type '"age"' is not assignable to parameter of type 'keyof typeof user'.(TypeScriptバージョンにより文言が異なる場合あり)
key に存在しない "age" を渡すと、K extends keyof T の制約に反するためエラーで止まります。戻り値の型も T[K] で正確に決まり、name を渡せば string が返ると分かります。
用法3:条件型(T extends U ? X : Y)
条件型は、T extends U ? X : Y で「T が U を満たすなら型 X、満たさなければ型 Y」と、型レベルで分岐させる仕組みです。三項演算子の型版だと考えると読みやすくなります。
type IsString<T> = T extends string ? "yes" : "no";
type A = IsString<string>; // "yes"(stringはstringを満たす)
type B = IsString<number>; // "no"(numberはstringを満たさない)
T extends string を「T は string の部分型か?」という真偽判定として読むのがコツです。IsString<string> はホバーすると "yes" に解決され、number なら "no" になります。
この仕組みは、関数の戻り値の型を取り出すといった実用的な型操作の土台にもなります。
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type R = MyReturnType<() => number>; // number
infer を使うと、条件が満たされたときにマッチした部分の型を取り出せます。条件型を書くときは、素朴に書く前に「ユニオンを渡したときに分配が起きないか」を確認しておくと、意図しない結果を避けられます。
分配的条件型の落とし穴と止め方
条件型で見落としやすいのが、ユニオン型を渡したときの挙動です。裸の型引数 T にユニオン A | B を渡すと、条件型は各メンバーに分配されて評価されます(分配的条件型)。
type ToArray<T> = T extends any ? T[] : never;
// string | number を渡すと、各メンバーに分配される
type R = ToArray<string | number>; // string[] | number[]((string | number)[] にはならない)
ToArray<string | number> は、string と number それぞれに条件型が適用され、結果も string[] | number[] というユニオンになります。これが分配の挙動です。
分配を止めたいときは、型引数を [T] のように角括弧でタプル化して、[T] extends [U] の形にします。これで T が「裸」でなくなり、ユニオンが丸ごと一つの型として評価されます。
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type R = ToArrayNonDist<string | number>; // (string | number)[]
タプルで囲むだけで分配が抑制され、(string | number)[] という一つの配列型になります。この分配と抑制の挙動は、TypeScript Handbook「Conditional Types」Distributive Conditional Types で説明されています。
よくある質問
Q1. extendsとimplementsはどちらを使えばいい?
親の型や実装を受け継いで拡張したいならextends、ある型を満たしているか検査だけしたいならimplementsを使います。class extends は実装を引き継ぎ、interface extends は型メンバーだけを引き継ぐのに対し、implementsは自分で実装が必要という点が選択の決め手です。
Q2. interfaceで複数の型を継承(extends A, B)できる?
できます。interfaceは interface C extends A, B のようにカンマ区切りで複数の型を継承できます。一方、クラスのextendsは単一の親しか指定できず、複数のクラスを同時に継承することはできません。型とクラスで可否が分かれる点に注意しましょう。
Q3. 条件型でneverが消えるのはなぜ?
分配的条件型に never を渡すと、never は空のユニオンとして扱われ、分配先が一つもないため結果も never(空)に畳まれます。分配を止めて never も一つの型として評価したいときは [T] extends [U] の形にします。
【付録】さらに学びを深めるためのリソース
さらにTypescriptの学習を進めたい方のために、いくつかのリソースを紹介します。
これらのリソースを活用することで、TypeScriptの型システムについてより深い知識を得ることができるでしょう。
おすすめの書籍
ゼロからわかる TypeScript入門
技術評論社から出版されている「ゼロからわかる TypeScript入門」は、プログラミング初心者や本職プログラマーではない方を主な対象にした入門書です。
変数・条件分岐・ループといった基本から、クラスやインターフェース、モジュールまで段階的に学べる構成になっています。最終章ではWeb APIとJSONを使った非同期Webアプリの作成も体験できるので、「実際に動くものを作る」ところまで到達できます。
プロを目指す人のためのTypeScript入門
技術評論社の「プロを目指す人のためのTypeScript入門 安全なコードの書き方から高度な型の使い方まで」、通称 ブルーベリー本 です。
JavaScriptの仕様とTypeScript独自の機能を両方押さえつつ、リテラル型・ユニオン型・keyof型・ジェネリクスなど、高度な型表現まで踏み込んで解説しています。TypeScriptの型システムの表現力を本格的に学べる一冊です。
オンラインで参照できる公式ドキュメント
TypeScript公式ハンドブック
https://www.typescriptlang.org/docs/
TypeScriptの公式ドキュメントです。
intersection型を含む、すべての型システムの機能について詳細な説明があります。
TypeScript Deep Dive
https://basarat.gitbook.io/typescript/
TypeScriptの深い部分まで掘り下げて解説しているオンラインブックです。
無料で読むことができ、intersection型についても詳しく説明されています。
TypeScriptの学習は終わりがありません。
新しい機能が常に追加され、より良い書き方が発見されています。
継続的に学習を続けることで、より良いTypeScriptプログラマーになれるはずです。
まとめ – extendsは「左が右を満たすか」の一語で3用法を貫く
この記事の要点をまとめます。
- extendsは文脈を問わず「左の型が右の型を満たすか」を表す一語で、継承・制約・条件型はその応用です。
- 用法1の継承(
class B extends A/interface B extends A)は、親の型や実装を受け継いで拡張します。 - 用法2の制約(
T extends U)は、型引数を「Uを満たす型」に限定し、安全な操作を増やします。 - 用法3の条件型(
T extends U ? X : Y)は、満たすかどうかで型を分岐させ、ユニオンでは分配が起きるため[T] extends [U]で止められます。
extendsを見かけたら「何が何を満たすと言っているのか」と問うだけで、3つのどの用法でも落ち着いて読み解けます。
※本記事の本文案はAIを活用して作成していますが、記載している内容およびコードは筆者が実際に調査、検証・実行し、内容の正確性を確認した上で公開しています。






