TypeScriptにおけるオブジェクト指向プログラミングの真髄は、柔軟性と堅牢性の両立にあります。

システムが大規模化し、コードの再利用性や一貫性が求められる現代の開発において、抽象クラス(abstract)は非常に重要な役割を果たします。

本記事では、TypeScriptにおける抽象クラスの基本概念から、インターフェースとの明確な使い分け、そして実戦で役立つ設計パターンまでを詳しく解説します。

TypeScriptの抽象クラスとは

抽象クラスは、他のクラスに継承されることを前提とした「クラスの設計図」のような存在です。

通常のクラスとは異なり、抽象クラス自体をインスタンス化(newによる生成)することはできません。

抽象クラスの主な目的は、共通の振る舞いを定義しつつ、一部の実装を派生クラス(サブクラス)に強制することにあります。

これにより、大規模なプロジェクトにおいても一貫したデータ構造とメソッドのシグネチャを維持することが可能になります。

抽象クラスの基本宣言

抽象クラスを定義するには、クラス名の前に abstract キーワードを付与します。

以下は、基本的な抽象クラスの宣言例です。

TypeScript
// 抽象クラスの定義
abstract class BaseLogger {
    // 具象メソッド(実装を持つメソッド)
    log(message: string): void {
        console.log(`[Log]: ${message}`);
    }

    // 抽象メソッド(実装を持たず、サブクラスに実装を強制する)
    abstract send(message: string): void;
}

// インスタンス化しようとするとエラーになる
// const logger = new BaseLogger(); // Error: Cannot create an instance of an abstract class.

インターフェースとの決定的な違いと使い分け

TypeScriptを学んでいると、「抽象クラスとインターフェースのどちらを使うべきか」という疑問に直面することがよくあります。

どちらも「クラスがどのような構造を持つべきか」を定義するものですが、その性質には大きな違いがあります。

以下の表で、主要な違いを整理しました。

機能抽象クラス(abstract class)インターフェース(interface)
インスタンス化不可不可(型定義のみ)
メソッドの実装可能(具象メソッド)不可(シグネチャのみ)
プロパティの初期化可能不可
アクセス修飾子使用可能(private, protectedなど)すべてpublic(指定不可)
継承の数単一継承のみ複数継承(実装)が可能

状態の保持と実装の共有

抽象クラスの最大の特徴は、「共通の実装」と「内部状態(プロパティ)」を保持できる点にあります。

すべてのサブクラスで共通して使用するロジックがある場合は、抽象クラスを選択するのが適切です。

一方で、単にオブジェクトの「形(型)」だけを定義したい場合や、複数の異なる機能を持たせたい場合はインターフェースが適しています。

アクセス修飾子の制御

抽象クラスでは、protected 修飾子を使用することで、「外部からはアクセスできないが、継承先のクラスからは利用できる」というカプセル化を実現できます。

これはインターフェースでは実現できない、抽象クラスならではの強力な機能です。

基本的な実装方法

抽象クラスを実際に活用するためには、継承(extends)とオーバーライドの仕組みを理解する必要があります。

抽象メソッドの定義とオーバーライド

抽象メソッドは、メソッドのシグネチャ(名前、引数、戻り値の型)のみを定義し、具体的な処理は記述しません。

サブクラスでは、この抽象メソッドを必ず実装しなければコンパイルエラーとなります。

TypeScript
abstract class Animal {
    constructor(public name: string) {}

    // 抽象メソッド
    abstract makeSound(): void;

    // 共通の処理
    move(): void {
        console.log(`${this.name}が移動しました。`);
    }
}

class Dog extends Animal {
    makeSound(): void {
        console.log("ワンワン!");
    }
}

class Cat extends Animal {
    makeSound(): void {
        console.log("ニャー!");
    }
}

const dog = new Dog("ポチ");
dog.makeSound();
dog.move();
実行結果
ワンワン!
ポチが移動しました。

抽象プロパティの活用

メソッドだけでなく、プロパティ自体にも abstract を付与することができます。

これにより、サブクラスに対して特定の値を設定することを強制できます。

TypeScript
abstract class Config {
    abstract readonly apiUrl: string;
    abstract readonly timeout: number;
}

class DevConfig extends Config {
    readonly apiUrl = "http://localhost:3000";
    readonly timeout = 5000;
}

class ProdConfig extends Config {
    readonly apiUrl = "https://api.example.com";
    readonly timeout = 1000;
}

実践的な設計パターン:Template Methodパターンの適用

抽象クラスの真価が発揮される設計パターンの代表例が Template Method(テンプレートメソッド)パターン です。

これは、アルゴリズムのスケルトン(骨組み)を抽象クラスで定義し、具体的なステップの実装をサブクラスに委ねる手法です。

共通フローの定義

例えば、データのインポート処理を考えてみましょう。

「ファイルを開く」「データをパースする」「データベースに保存する」という一連の流れは共通ですが、パースの方法はファイル形式(CSVやJSON)によって異なります。

TypeScript
abstract class DataImporter {
    // テンプレートメソッド:一連の流れを定義
    public async import(filePath: string): Promise<void> {
        this.openFile(filePath);
        const data = this.parseData();
        await this.saveToDatabase(data);
        this.closeFile();
    }

    private openFile(path: string): void {
        console.log(`${path} を開きました。`);
    }

    private closeFile(): void {
        console.log("ファイルを閉じました。");
    }

    // サブクラスで実装を強制するステップ
    protected abstract parseData(): any[];

    // 共通のDB保存処理
    private async saveToDatabase(data: any[]): Promise<void> {
        console.log(`${data.length}件のデータを保存しました。`);
    }
}

class CsvImporter extends DataImporter {
    protected parseData(): any[] {
        console.log("CSVをパースしています...");
        return [1, 2, 3]; // ダミーデータ
    }
}

class JsonImporter extends DataImporter {
    protected parseData(): any[] {
        console.log("JSONをパースしています...");
        return [{ id: 1 }, { id: 2 }]; // ダミーデータ
    }
}

実行結果の確認

このパターンを使用することで、全体のフローを制御しつつ、詳細な実装のみを差し替えることが可能になります。

TypeScript
const csvImporter = new CsvImporter();
csvImporter.import("data.csv");

const jsonImporter = new JsonImporter();
jsonImporter.import("data.json");
実行結果
data.csv を開きました。
CSVをパースしています...
3件のデータを保存しました。
ファイルを閉じました。
data.json を開きました。
JSONをパースしています...
2件のデータを保存しました。
ファイルを閉じました。

2026年のモダンな開発における抽象クラスの役割

2026年現在の開発トレンドにおいても、抽象クラスは 「クリーンアーキテクチャ」「ドメイン駆動設計(DDD)」 のコンテキストで頻繁に使用されます。

特に、依存性の注入(DI)を利用する際、インターフェースだけでは表現しきれない「基底の振る舞い」を定義するために抽象クラスが重宝されます。

また、TypeScriptの最新バージョンでは、型推論の強化により、抽象クラスを継承した際の型補完がより強力になっています。

これにより、開発者は詳細な型定義を意識することなく、安全に抽象クラスを活用できるようになりました。

抽象クラス利用時の注意点とアンチパターン

非常に便利な抽象クラスですが、誤った使い方をするとコードの複雑性を高めてしまいます。

まず、「継承の階層を深くしすぎない」ことが鉄則です。

階層が3層、4層と深くなると、基底クラスの変更がすべてのサブクラスに予期せぬ影響を及ぼす「脆い基底クラスの問題」が発生します。

また、実装を共有する必要が全くない場合は、抽象クラスではなくインターフェースを使用すべきです。

「Composition over Inheritance(継承より合成)」 という原則を常に意識し、継承が本当に適切な手段であるかを検討してください。

抽象クラスは「Is-a関係(サブクラスは基底クラスの一種である)」が明確に成り立つ場合にのみ使用するのがベストプラクティスです。

まとめ

TypeScriptの抽象クラスは、一貫性のあるクラス設計を実現するための強力なツールです。

インターフェースとの違いを理解し、適切に使い分けることで、メンテナンス性の高いコードベースを構築できます。

特に Template Method パターンのような設計手法を取り入れることで、重複コードを排除し、堅牢なシステムを開発することが可能になります。

継承の乱用には注意が必要ですが、正しく活用すれば、2026年以降の複雑なアプリケーション開発においても大きな武器となるでしょう。

まずは小さな共通処理を抽象クラスにまとめることから始めて、その利便性を実感してみてください。