TypeScriptのクラス継承:extends・super・abstractの使い方
TypeScriptのクラスの継承を実行例で解説。extends・super・override・protected・抽象クラスの役割、親の契約、委譲との使い分けを学べます。
クラスの継承は、既存のクラスを基にして、機能を追加したクラスを定義する仕組みです。TypeScriptでは extends を使います。共通の状態や振る舞いを引き継げますが、子クラスは親クラスとして使った場合の契約も満たす必要があります。
この記事では、super、メソッドのオーバーライド、protected、抽象クラスを実行例で確認します。各節は独立したファイルです。インストール記事の環境を前提に、TypeScript 4.9.4の strict 設定で確認しています。
継承先で振る舞いを拡張する
親クラスを継承して機能を追加する
src/inheritance-basic.ts を作成します。
export {};
class Person {
constructor(public readonly name: string) {}
introduce(): string {
return `私の名前は${this.name}です`;
}
}
class Student extends Person {
constructor(name: string, public readonly studentId: number) {
super(name);
}
study(): string {
return `${this.name}は学習中です(学籍番号: ${this.studentId})`;
}
}
const student = new Student("葵", 101);
console.log(student.introduce()); // 私の名前は葵です
console.log(student.study()); // 葵は学習中です(学籍番号: 101)
Person が親クラス、Student が子クラスです。子クラスでコンストラクタを定義する場合は、super(...) で親クラスのコンストラクタを呼び出します。this を使う前に呼び出す必要があります。
Student は親クラスの name と introduce を利用し、独自の studentId と study を追加しています。公式のクラスの継承の説明
メソッドをオーバーライドする
同じ名前のメソッドを子クラスに定義して、振る舞いを変更できます。src/inheritance-override.ts に保存します。
export {};
class Person {
constructor(public readonly name: string) {}
introduce(): string {
return `私の名前は${this.name}です`;
}
}
class Employee extends Person {
constructor(name: string, public readonly role: string) {
super(name);
}
override introduce(): string {
return `${super.introduce()}。担当は${this.role}です`;
}
}
const person: Person = new Employee("葵", "開発");
console.log(person.introduce());
// 私の名前は葵です。担当は開発です
変数の型を Person としても、実際のオブジェクトが Employee であれば、子クラスの introduce が呼ばれます。super.introduce() を使うと、親クラスの実装を呼び出して結果を再利用できます。
override は「親に同じメンバーが存在する」ことをコンパイラへ確認させます。親メソッドが引数なしで呼べるのに、子メソッドへ必須引数を追加するなど、親の契約に適合しない変更はできません。型による自動的な実装選択を意味するオーバーロードとは別の概念です。
noImplicitOverride を有効にすると、オーバーライドしているメンバーへの override の付け忘れも検出できます。このオプションは strict とは別に指定します。公式の noImplicitOverride の説明
protectedで子クラスに必要な情報を渡す
protected はクラス内部と継承先から参照するメンバーに使います。src/inheritance-protected.ts の例です。
export {};
class LabeledItem {
constructor(protected readonly label: string) {}
}
class MenuItem extends LabeledItem {
render(): string {
return `[${this.label}]`;
}
}
const menu = new MenuItem("記事一覧");
console.log(menu.render()); // [記事一覧]
外部のTypeScriptコードから menu.label を直接読むことはできません。一方、MenuItem のメソッド内では参照できます。private で宣言したメンバーは、その宣言元のクラスの外から直接参照できません。
これらは型チェック上のアクセス制限です。実行時にもプライベートなフィールドを必要とする場合は、JavaScriptの # 付きフィールドという別の仕組みがあります。
抽象クラスで子クラスに実装を求める
共通部分だけを実装し、具体的な処理を子クラスへ任せたいときは abstract を使えます。src/inheritance-abstract.ts を作成します。
export {};
abstract class Notification {
constructor(protected readonly recipient: string) {}
abstract createMessage(): string;
preview(): string {
return `${this.recipient}宛: ${this.createMessage()}`;
}
}
class WelcomeNotification extends Notification {
createMessage(): string {
return "登録ありがとうございます";
}
}
const notification = new WelcomeNotification("葵");
console.log(notification.preview()); // 葵宛: 登録ありがとうございます
抽象クラスは、TypeScriptから直接インスタンス化できません。具体的な子クラスは、抽象メソッドを実装します。この例はメッセージを表示するだけで、メールなどの送信は行いません。
サンプルを実行する
npx tsc src/inheritance-basic.ts --strict --noImplicitOverride --target ES2020 --module NodeNext --outDir dist --noEmitOnError
node dist/inheritance-basic.js
他の節はファイル名を変更して実行できます。同じ名前のクラスを使う節があるため、例同士を一つのファイルに結合しないでください。
継承は、子クラスを親クラスとして自然に扱える関係に適しています。処理を再利用したいだけなら、別のオブジェクトを受け取って仕事を任せる「委譲」も選択肢です。継承階層を深くする前に、変更の影響が追いやすい構成かを確認しましょう。
継承を使う前に確認する設計条件
親クラスとして扱っても成立するか
子クラスだけが特別な前提を要求すると、親クラスを受け取る関数で予期しない失敗が起きます。引数の条件、戻り値、失敗時の状態が親の契約と矛盾しないことを確認します。
単なる共通処理なら委譲も選べる
同じ計算を使うためだけに継承階層を増やす必要はありません。関数や別オブジェクトへ処理を任せる構成なら、役割ごとの変更とテストを分けられます。