Menu

TypeScriptのクラス継承: extends・super・override

TypeScriptのクラス継承を解説します。extends と super、型の互換性を保ったメソッドのオーバーライド、override キーワードと noImplicitOverride、protected メンバー、declare を使ったフィールドの再宣言、implements のほうが向いている場面です。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

TypeScriptのクラスは extends で別のクラスを継承します。サブクラスは親のすべてのフィールドとメソッドを受け継ぎ、super(...) で親のコンストラクターを呼び、メソッドをオーバーライドできます。TypeScriptは、各オーバーライドが親の型と互換性を保っているかをチェックします。

これはJavaScriptのクラス継承です(その裏にあるプロトタイプチェーンについては JavaScriptの継承 を参照してください)。TypeScriptが加えるのは型チェックです。Dog は Animal が期待される場所ならどこにでも代入でき、コンパイラーはオーバーライドと super の呼び出しを検証します。

メソッドのオーバーライド

同じ名前のサブクラスのメソッドは、親のメソッドを置き換えます。その型は親の型に代入できなければなりません。引数は同じか互換で、戻り値の型は同じかより狭い型です。そうでないと、Animal を持っているコードが、サブクラスには扱えない引数でメソッドを呼べてしまいます。

引数を value: string に変えるとエラー TS2416: Property 'format' in type 'CurrencyFormatter' is not assignable to the same property in base type 'Formatter' になります。理由はループを見るとわかります。どのサブクラスかを知らないまま、すべての要素に対して数値で format を呼んでいるからです。

穴が1つあります。メソッドの引数は双変で比較されるので、format(value: 1 | 2) のようにより狭い型を受け取るオーバーライドもコンパイルできてしまいます。ループからは 1234567 が渡されうるのにです。戻り値の型は厳密にチェックされ、string の代わりに number を返すと TS2416 になります。

override キーワードと noImplicitOverride

override は「このメンバーは基底クラスのメンバーを置き換える」という宣言で、コンパイラーはその約束を守らせます。基底クラスにそのメンバーがなければ(打ち間違いや、誰かが親でメソッド名を変えた場合)エラーになります:

index.ts(9,14): error TS4117: This member cannot have an 'override' modifier because it is not declared in the base class 'Animal'. Did you mean 'speak'?

override がなければ、この打ち間違いはコンパイルでき、speak() は黙って "..." を返します。キーワードは書いた場所でしか役に立たないので、tsconfig.json で noImplicitOverride を有効にしましょう。そうすると、基底クラスのメンバーをオーバーライドするメソッドはすべてそれを明示する必要があり、サブクラスの普通の speak() はエラー TS4114(This member must have an 'override' modifier because it overrides a member in the base class 'Animal')になります。

{
    "compilerOptions": {
        "strict": true,
        "noImplicitOverride": true
    }
}

noImplicitOverride は strict に含まれていないので、自分で追加する必要があります。override はコンパイル後のJavaScriptからは消えます。

サブクラスの中の protected メンバー

親の private メンバーはサブクラスからは見えません。protected メンバーは見えます。これが protected の存在理由で、親クラスが子クラスに提供するAPIです。

private も protected も実行時には強制されない理由を含め、ルールの全体は アクセス修飾子 を参照してください。

フィールドをより狭い型で再宣言する

サブクラスは、フィールドが親の宣言より具体的なものを持つことを知っている場合がよくあります。それを pet: Dog と再宣言すると、現在のクラスフィールドの仕様では実行時に影響があります。super() が戻ったあとにサブクラスのフィールドが undefined で初期化され、親のコンストラクターが保存した値が消えてしまいます。サブクラスのコンストラクターが再び代入しない限り、TypeScriptはこれを TS2612(Property 'pet' will overwrite the base property in 'Home')として報告します。型だけを変えるには declare を使います:

declare は何も出力しないので、Home のコンストラクターが設定した値は残ります。より狭い型を本当にしているのは、Dog しか受け付けないコンストラクターです。

implements と extends

2つのキーワードは別の疑問に答えるもので、クラスは両方を使えます。

extendsimplements
対象クラス1つインターフェース(またはオブジェクト型の型エイリアス)をいくつでも
コードを持ち込むかはい: フィールド、メソッド、コンストラクターいいえ、形をチェックするだけ
実行時に存在するかはい、プロトタイプチェーンいいえ、消去される
instanceof が使えるかはいいいえ
よくある用途実際の振る舞いを共有するコードの残りの部分に形を約束する

クラスが継承できるクラスは1つだけです(TS1174: Classes can only extend a single class)。複数のところから振る舞いを組み合わせるには、複数のインターフェースを実装し、コンポジション(ヘルパーオブジェクトを持つフィールド)かミックスインでコードを共有します。

ミックスイン

ミックスインは、クラスを受け取ってそのサブクラスを返す関数です。型チェックの効く、多重継承のようなものが実現できます:

Constructor の中の any[] は必須です。TypeScriptがミックスインの基底として受け付けるのは、any[] 型の残余引数を1つだけ取るコンストラクターだけです。ミックスインは動きますが、型とスタックトレースが読みにくくなるので、まずはコンポジションを検討しましょう。

よくある間違い

  • 基底クラスのコンストラクターからオーバーライド可能なメソッドを呼ぶ。 基底クラスのコンストラクターはサブクラスのフィールドが初期化される前に実行されるので、this.items を読むオーバーライドは undefined を見て、this.items.push(x) のような呼び出しは実行時に TypeError を投げます。TypeScriptはこれを捕まえません。
  • super(...) を忘れる、または遅く呼ぶ。 エラー TS2377 と TS17009 です。サブクラスが this に触れる前に、親がオブジェクトを作らなければなりません。
  • declare なしで親のフィールドを再宣言する。 サブクラスのフィールドが値を undefined にリセットしてしまいます。
  • 深い階層。 階層が1段増えるたびに、子は親の内部に依存します。たいていは2段で十分で、それ以上ならコンポジションかインターフェースを選びましょう。

よくある質問

TypeScriptの継承はどのように動きますか?

クラスは extends で別のクラスを継承します: class Dog extends Animal。サブクラスは親のフィールドとメソッドを受け継ぎ、super(...) で親のコンストラクターを呼び、メソッドをオーバーライドできます。TypeScriptは、オーバーライドが親と型の互換性を保っているかをチェックします。

TypeScriptの override キーワードは何をしますか?

override は、基底クラスのメンバーを置き換えるメソッドやプロパティに付けます。基底クラスにその名前のメンバーがなければ、コンパイラーは TS4117 を報告するので、打ち間違いや親で名前が変わったメンバーに気づけます。noImplicitOverride を有効にすると、すべてのオーバーライドにこのキーワードが必要になります(ないと TS4114)。

TypeScriptで親クラスのメソッドを呼ぶには?

サブクラスのメソッドの中で super.methodName(...) を使います。コンストラクターの中では super(...) が親のコンストラクターを呼び、this を使う前に実行しなければなりません。

TypeScriptは多重継承に対応していますか?

対応していません。クラスが継承できるクラスは1つだけです(TS1174: Classes can only extend a single class)。インターフェースはいくつでも実装でき、ミックスイン(クラスを受け取ってサブクラスを返す関数)で複数のところから振る舞いを組み合わせられます。

extends と implements の違いは何ですか?

extends は親クラスから実際のコードを継承します。implements はクラスがインターフェースの形に合っているかをチェックするだけで、実行時には何も追加しません。クラスは両方を使えます: class Dog extends Animal implements Pet。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める