interface Child extends Parent は、Parent のすべてのメンバーと自分のメンバーを持つ新しいインターフェースを作ります。子の型の値は、親のプロパティをすべて持っているので、親が期待される場所ならどこでも使えます。
TypeScriptでは同じキーワードが4つの場所に現れ、意味は関連していますが違います:
| 場所 | 例 | 意味 |
|---|---|---|
| インターフェース | interface Dog extends Animal {} | 親のメンバーを継承する |
| クラス | class Dog extends Animal {} | フィールドとメソッドの実装を継承する(実行時のJavaScript) |
| ジェネリクスのパラメーター | <T extends Animal> | 制約: 型引数は Animal に代入できなければならない |
| 条件型 | T extends string ? A : B | テスト: T は string に代入できるか |
複数のインターフェースを拡張する
extends のあとに複数の親をカンマ区切りで並べます。結果はそのすべてのメンバーを持ちます:
interface Identified {
id: number;
}
interface Timestamped {
createdAt: Date;
updatedAt: Date;
}
interface Post extends Identified, Timestamped {
title: string;
}
// Post requires id, createdAt, updatedAt and title
2つの親が同じプロパティを同一でない型で宣言していると、子はエラー TS2320(Interface 'Post' cannot simultaneously extend types 'A' and 'B')になります。ただし、子がそのプロパティを両方に代入できる型で再宣言すれば別です。
型エイリアスを拡張する
type エイリアスには extends 句がありません。交差型 & で両辺のメンバーを組み合わせて拡張します。インターフェースはオブジェクト型のエイリアスを直接拡張することもでき、型エイリアスはインターフェースと交差させられます。2種類は自由に混ぜられます。
2つの方法は、プロパティが衝突するまでは同じように動きます。extends は衝突するプロパティを宣言の位置で報告し、& は黙ってそれを never にします。比較は interface と type のページに、& の詳細は交差型のページにあります。
プロパティの型を上書きする
子のインターフェースは親のプロパティを再宣言できますが、親の型に代入できる型でだけです。string をリテラルに狭めたり、ユニオン型から null を取り除いたりするのは許されます。無関係な型に変えることは許されません:
index.ts(13,11): error TS2430: Interface 'Label' incorrectly extends interface 'Shape'.
Types of property 'kind' are incompatible.
Type 'number' is not assignable to type 'string'.
このルールがあるのは、Label を Shape が期待される場所のどこでも使えるようにするためです。kind が数値になりうるなら、Shape 向けに書かれた shape.kind.toUpperCase() を呼ぶコードが壊れてしまいます。
Omit でプロパティを置き換える
継承したプロパティに本当に別の型が必要なときは、先に Omit で親から取り除き、それから新しいプロパティを宣言します。よくあるのは、ID や日付が文字列で届くAPIのデータを変換する場合です:
Omit<ApiUser, "id" | "createdAt"> は { name: string } なので、新しい id と createdAt は何とも衝突しません。Pick、Partial など、ほかのユーティリティ型も同じように拡張できます。
クラスの extends
class Dog extends Animal はJavaScriptのクラス継承で、インターフェースの extends と違って実行時に存在します。Dog は Animal のコンストラクター、フィールド、メソッドの実装を受け取り、new Dog(...) instanceof Animal は true です。TypeScriptはその上に、override キーワードのような型チェックを加えます:
クラスが extends できるクラスは1つだけですが、インターフェースはいくつでも implements できます。implements は何もコピーせず、インターフェースが要求するものをクラスが宣言しているかをチェックするだけです。super、protected メンバー、override については 継承 で説明しています。
ジェネリクスと条件型の extends
山かっこの中の extends は制約です。<T extends { length: number }> は数値の length を持つあらゆる型引数を受け付け、関数の本体で .length を使えるようにします。条件型の T extends U ? X : Y は、T が U に代入できるかを問います。
どちらの場所でも extends は「代入できる」という意味で、インターフェースの形がチェックする関係と同じです。制約については ジェネリクスの制約 で、型レベルのテストについては 条件型 で詳しく扱います。
よくある質問
TypeScriptでインターフェースを拡張するには?
interface Child extends Parent { ... } と書きます。子は親のすべてのプロパティと、自分で宣言したプロパティを持ちます。複数のインターフェースを一度に拡張するには、カンマで区切ります: interface C extends A, B { ... }。
TypeScriptで型エイリアスを拡張するには?
型エイリアスでは extends を使えないので、代わりに交差型で型を組み合わせます: type Admin = User & { permissions: string[] }。インターフェースはオブジェクト型のエイリアスを直接拡張することもできます: interface Admin extends User { permissions: string[] }。
インターフェースを拡張するときにプロパティの型を上書きできますか?
互換性のある、より狭い型でだけできます。kind: string を kind: "circle" に置き換えることはできます。string のプロパティに number を使うような違う型はエラー TS2430(Interface 'X' incorrectly extends interface 'Y')です。無関係な型でプロパティを置き換えるには、先にそれを取り除きます: interface User extends Omit<ApiUser, "id"> { id: number }。
extends と implements の違いは何ですか?
extends は継承します。インターフェースは親のメンバーを受け取り、クラスは親クラスのフィールドとメソッドの実装を受け取ります。implements はチェックするだけです。インターフェースを実装するクラスはすべてのメンバーを自分で宣言しなければならず、インターフェースからは何も得ません。
<T extends string> のようなジェネリクスの extends はどういう意味ですか?
継承ではなく制約です。T extends string は、型引数が string に代入できなければならないという意味なので、関数の中で T に文字列のメソッドを使えます。条件型の T extends U ? X : Y はテストで、T が U に代入できれば結果は X、そうでなければ Y です。