TypeScriptは、現代のフロントエンドおよびバックエンド開発において欠かせない言語となり、その型システムの柔軟性は日々進化を続けています。

大規模なアプリケーションを構築する際、コードの再利用性や拡張性を維持するために「継承」という概念を正しく理解し、活用することは非常に重要です。

本記事では、TypeScriptにおける継承(extends)の基本的な仕組みから、実践的なクラス設計のパターンまでを詳しく解説します。

オブジェクト指向プログラミングの基礎を固めつつ、最新の開発シーンで求められる柔軟なコードの書き方を学んでいきましょう。

TypeScriptにおける継承(extends)の基本概念

継承とは、あるクラスが持つプロパティやメソッドを別のクラスに引き継ぐ仕組みのことです。

TypeScriptでは、JavaScriptのES6から導入されたクラス構文をベースに、型安全な継承を実現しています。

継承を利用することで、共通の機能を一つの「親クラス(基底クラス)」にまとめ、具体的な振る舞いを「子クラス(派生クラス)」で定義できるようになります。

継承の基本的な構文

TypeScriptで継承を行うには、extends キーワードを使用します。

以下のコードは、動物を表す親クラスを継承して、特定の動物クラスを作成する基本的な例です。

TypeScript
// 親クラス(基底クラス)の定義
class Animal {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  move(distanceInMeters: number = 0) {
    console.log(`${this.name} moved ${distanceInMeters}m.`);
  }
}

// 子クラス(派生クラス)の定義
class Dog extends Animal {
  bark() {
    console.log("Woof! Woof!");
  }
}

const dog = new Dog("Pochi");
dog.bark();
dog.move(10);
実行結果
Woof! Woof!
Pochi moved 10m.

この例では、Dog クラスが Animal クラスを継承しており、Animal クラスで定義された move メソッドをそのまま利用できています。

継承によって、重複するコードを削減し、論理的な親子関係をコード上で表現することが可能です。

extendsを使ったクラス継承の詳細な仕組み

クラスの継承を深く理解するためには、親クラスから何が引き継がれ、どのように上書きできるかを知る必要があります。

superキーワードの役割と注意点

子クラスでコンストラクタを定義する場合、必ず super() を呼び出さなければなりません。

super() は親クラスのコンストラクタを呼び出すための関数であり、これによって親クラスの初期化処理が実行されます。

super()を呼び出す前に this を参照することはできないというルールに注意してください。

TypeScript
class Bird extends Animal {
  canFly: boolean;

  constructor(name: string, canFly: boolean) {
    // 親クラスのコンストラクタを呼び出す
    super(name);
    this.canFly = canFly;
  }

  fly() {
    if (this.canFly) {
      console.log(`${this.name} is flying!`);
    } else {
      console.log(`${this.name} cannot fly.`);
    }
  }
}

const penguin = new Bird("Penguin", false);
penguin.fly();
実行結果
Penguin cannot fly.

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

子クラスでは、親クラスが持つメソッドを独自の処理で上書き(オーバーライド)することができます。

オーバーライドを行うことで、共通のインターフェースを保ちつつ、特定のクラス固有の挙動を実装できます。

TypeScript
class Snake extends Animal {
  // moveメソッドをオーバーライド
  move(distanceInMeters = 5) {
    console.log("Slithering...");
    super.move(distanceInMeters);
  }
}

const snake = new Snake("Snakey");
snake.move();
実行結果
Slithering...
Snakey moved 5m.

super.move() のように、親クラスの元のメソッドを呼び出すことも可能です。

アクセス修飾子によるカプセル化と継承

継承において、親クラスのプロパティを子クラスにどこまで公開するかを制御するために、アクセス修飾子を利用します。

TypeScriptには、publicprotectedprivate の3種類の修飾子が存在します。

修飾子クラス内部子クラス内部インスタンス(外部)
public○ アクセス可○ アクセス可○ アクセス可
protected○ アクセス可○ アクセス可× アクセス不可
private○ アクセス可× アクセス不可× アクセス不可

継承先の子クラスでのみ使用させたいプロパティには protected を使用するのが一般的です。

TypeScript
class BaseUser {
  protected id: string;

  constructor(id: string) {
    this.id = id;
  }
}

class AdminUser extends BaseUser {
  showId() {
    // 子クラスからはprotected属性にアクセス可能
    console.log(`Admin ID: ${this.id}`);
  }
}

const admin = new AdminUser("A001");
// admin.id はエラーになる(外部からはアクセス不可)

インターフェースの継承と実装

TypeScriptでは、クラスだけでなくインターフェースも継承することができます。

インターフェースの継承は、型の構造を再利用するために非常に強力な手段です。

インターフェース同士の継承

複数のインターフェースを組み合わせて新しいインターフェースを作成することができます。

TypeScript
interface Person {
  name: string;
}

interface Employee extends Person {
  employeeId: number;
}

const newStaff: Employee = {
  name: "Tanaka",
  employeeId: 1234
};

クラスの継承とは異なり、インターフェースは複数のインターフェースを同時に継承(多重継承)することが可能です。

クラスによるインターフェースの実装

implements キーワードを使用することで、クラスに特定のインターフェースが定義する構造を強制することができます。

TypeScript
interface Serializable {
  serialize(): string;
}

class UserProfile implements Serializable {
  constructor(public username: string) {}

  serialize() {
    return JSON.stringify({ username: this.username });
  }
}

2026年における最新のクラス設計パターン

現代のTypeScript開発においては、単純な継承だけでなく、より柔軟性の高い設計パターンが推奨されるようになっています。

抽象クラス(abstract class)の活用

抽象クラスは、直接インスタンス化することができないクラスです。

子クラスに対して特定のメソッドの実装を強制するために使用されます。

TypeScript
abstract class Shape {
  abstract getArea(): number; // 子クラスで必ず実装する必要がある

  printArea() {
    console.log(`Area is: ${this.getArea()}`);
  }
}

class Circle extends Shape {
  constructor(private radius: number) {
    super();
  }

  getArea() {
    return Math.PI * this.radius ** 2;
  }
}

抽象クラスを使うことで、共通のロジック(printArea)を持ちつつ、計算方法などの詳細(getArea)を子クラスに委ねることができます。

継承(Inheritance)か委譲(Composition)か

クラス設計において、「継承よりも委譲を好む(Composition over Inheritance)」という原則が強調されています。

深い継承ツリーは、親クラスの変更が予期せぬ範囲に影響を及ぼす「脆弱な基底クラス問題」を引き起こす可能性があります。

「AはBである(is-a関係)」なら継承を、「AはBを持っている(has-a関係)」なら委譲を検討しましょう。

例えば、「スズメは鳥である」なら継承が適切ですが、「ユーザーは住所を持っている」なら、ユーザーが住所クラスをプロパティとして保持する「委譲」が適切です。

継承を使用する際のベストプラクティス

継承を正しく活用し、メンテナンス性の高いコードを書くためのポイントを整理します。

1. 継承の深さを最小限に抑える

継承の階層が3段階、4段階と深くなると、コードの追跡が困難になります。

可能な限りフラットな構造を目指し、共通機能が多すぎる場合は、機能を小さなモジュールに分割することを検討してください。

2. readonly 修飾子の活用

継承先で書き換えられたくない定数のようなプロパティには、readonly を付与します。

これにより、意図しない値の変更を防ぎ、堅牢なクラス設計が可能になります。

3. 型の整合性を保つ

子クラスでメソッドをオーバーライドする際は、親クラスの引数や戻り値の型と互換性があることを確認してください。

TypeScriptのコンパイラは型チェックを行ってくれますが、論理的な意味が壊れないように設計することが重要です。

実戦的なユースケース:APIレスポンスの基底クラス

実際の開発現場では、APIのレスポンス処理などで継承がよく使われます。

TypeScript
abstract class BaseApiResponse {
  constructor(public status: number, public message: string) {}

  isSuccess(): boolean {
    return this.status >= 200 && this.status < 300;
  }
}

class UserResponse extends BaseApiResponse {
  constructor(status: number, message: string, public userData: { id: number, name: string }) {
    super(status, message);
  }
}

const response = new UserResponse(200, "Success", { id: 1, name: "Alice" });
if (response.isSuccess()) {
  console.log(response.userData.name);
}
実行結果
Alice

このように、共通のステータスチェック処理を基底クラスに持たせることで、個別のレスポンスクラスではデータの中身だけに集中できます。

まとめ

TypeScriptにおける継承(extends)は、コードを整理し再利用性を高めるための非常に強力なツールです。

基本的な extends の使い方から super の呼び出し、アクセス修飾子の活用方法までをマスターすることで、型安全で堅牢なアプリケーションを設計できるようになります。

一方で、継承の多用はコードを複雑にするリスクも孕んでいます。

「is-a関係」を意識し、必要に応じて抽象クラスや委譲を組み合わせることが、洗練されたクラス設計への鍵となります。

2026年現在の開発環境においても、これらの基本原則は変わらず重要です。

本記事を参考に、TypeScriptの継承を適切に使いこなし、質の高いコードベースを構築していきましょう。