TypeScriptで抽象クラスを完全理解!実用例10選でプロへの道を切り開く

TypeScript抽象クラスマスターの道 TypeScript
この記事は約31分で読めます。

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

基本的な知識があればサンプルコードを活用して機能追加、目的を達成できるように作ってあります。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を凌駕する現役のプログラマチームによって監修されています。

サイト内のコードを共有する場合は、参照元として引用して下さいますと幸いです

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

TypeScriptは、JavaScriptに静的型付けの特性を追加した言語として、多くの開発者からの支持を受けています。

この言語の高度な特性の一つが、抽象クラスです。

今回の記事では、TypeScriptの抽象クラスの詳細な使い方と、その活用法を、実用的なサンプルコード10選とともに紹介します。

抽象クラスは、オブジェクト指向プログラミングにおける一般的な概念であり、TypeScriptでのその扱いを理解することで、より高度なプログラム設計が可能となります。

特に、初心者の方はサンプルコードを手元で試しながら、実践的な学習を進めることをおすすめします。

●TypeScriptの抽象クラスとは?

TypeScriptはJavaScriptに強力な型付け機能を加えたプログラミング言語で、その中には抽象クラスという特別なクラスも含まれています。

一般的に、クラスはオブジェクト指向プログラミングにおける「設計図」の役割を果たし、そのクラスから実際のオブジェクトを生成します。

しかし、抽象クラスは少し異なります。

抽象クラスは、他のクラスで実装されるべき基本的な構造や機能を定義するクラスです。

つまり、直接インスタンス化することはできませんが、他のクラスがこの抽象クラスを継承することによってその機能を利用することができます。

○抽象クラスの基本概念

抽象クラスを定義する際には、abstractキーワードを使用します。

このキーワードを付けることで、そのクラスが抽象クラスであることを明示的に示すことができます。

抽象クラスの基本的な定義の一例を紹介します。

// 抽象クラスの定義
abstract class 動物 {
    // 抽象メソッド
    abstract 鳴く(): void;
}

// 犬クラスの定義
class 犬 extends 動物 {
    鳴く() {
        console.log("ワンワン");
    }
}

このコードでは、動物という抽象クラスを定義しています。

そして、その中に鳴くという抽象メソッドを持っています。具体的な動物、例えばクラスがこの抽象クラスを継承し、鳴くメソッドを実装しています。

この例では、犬が「ワンワン」と鳴くことを示しています。

このクラスのインスタンスを生成し、鳴くメソッドを呼び出すと、「ワンワン」という結果が得られます。

具体的には次のようになります。

const ポチ = new 犬();
ポチ.鳴く();  // 出力結果: ワンワン

抽象クラスの強力な点は、一貫したインターフェースを提供しつつ、具体的な実装をサブクラスに任せることができる点にあります。

これにより、異なるクラス間での共通の振る舞いや構造を簡単に管理することができます。

●抽象クラスの実用的な使い方

抽象クラスは、直接インスタンス化できない特殊なクラスであり、具体的な実装がないメソッド(抽象メソッド)を持つことができます。

この特性を活かし、オブジェクト指向の設計において、特定のベース機能を持つクラスの「ひな型」や「基底」として利用されます。

ここでは、TypeScriptでの抽象クラスの実用的な使い方と具体的なサンプルコードを紹介します。

○サンプルコード1:基本的な抽象クラスの定義と利用

このコードでは、動物を表す抽象クラスを定義し、それを継承した具体的な動物のクラスを作成しています。

この例では、抽象クラスを基底として、具体的な動物の動作を実装しています。

abstract class Animal {
    name: string;

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

    // 抽象メソッドの定義
    abstract speak(): string;
}

class Dog extends Animal {
    speak(): string {
        return "ワンワン";
    }
}

class Cat extends Animal {
    speak(): string {
        return "ニャーニャー";
    }
}

const dog = new Dog("ポチ");
console.log(`${dog.name}の鳴き声: ${dog.speak()}`);

const cat = new Cat("たま");
console.log(`${cat.name}の鳴き声: ${cat.speak()}`);

上記のコードを実行すると、ポチの鳴き声: ワンワンおよびたまの鳴き声: ニャーニャーという出力が得られます。

○サンプルコード2:抽象メソッドの定義と実装

このコードでは、抽象クラス内で抽象メソッドを定義し、その後、具体クラスでこのメソッドの実装を行う例を表しています。

この例では、車の動きを抽象的に表現しています。

abstract class Car {
    abstract startEngine(): void;
    abstract stopEngine(): void;
}

class Toyota extends Car {
    startEngine() {
        console.log("トヨタのエンジンが始動します");
    }
    stopEngine() {
        console.log("トヨタのエンジンが停止します");
    }
}

const myCar = new Toyota();
myCar.startEngine(); // トヨタのエンジンが始動します
myCar.stopEngine();  // トヨタのエンジンが停止します

このコードを実行すると、エンジンの始動と停止のメッセージが表示されます。

○サンプルコード3:抽象クラスを継承した具体クラスの作成

TypeScriptにおいて、抽象クラスは直接インスタンス化することができません。

それでは、抽象クラスの真価を発揮するためには、どのように活用すればよいのでしょうか。

その答えは、「具体クラス」として抽象クラスを継承することです。

具体クラスは、抽象クラスが提供する抽象メソッドの具体的な実装を持つクラスを指します。

具体クラスを作成することで、抽象クラスの基盤となる設計を元に、具体的な動作や処理を持ったクラスを手に入れることができます。

このコードでは、抽象クラス「Animal」を定義し、その抽象クラスを継承した具体クラス「Dog」と「Cat」を作成しています。

この例では、Animalクラスの抽象メソッド「speak」をDogクラスとCatクラスで異なる方法で実装しています。

abstract class Animal {
    // 抽象メソッド:speak
    abstract speak(): string;
}

// Dogクラス:Animalクラスを継承
class Dog extends Animal {
    speak(): string {
        return 'ワンワン';
    }
}

// Catクラス:Animalクラスを継承
class Cat extends Animal {
    speak(): string {
        return 'ニャー';
    }
}

const dog = new Dog();
const cat = new Cat();
console.log(dog.speak()); // ワンワン
console.log(cat.speak()); // ニャー

こちらのコードを実行すると、「ワンワン」と「ニャー」という出力が得られます。

このように、同じ抽象クラスを継承しながらも、具体クラスごとに異なる振る舞いを定義することができるのが、抽象クラスの強力な点といえるでしょう。

○サンプルコード4:抽象クラス内でのプロパティの定義

TypeScriptでは、クラスにプロパティやメソッドを定義することができるように、抽象クラス内でもプロパティを定義できます。

しかし、抽象クラスでのプロパティの扱いは、通常のクラスとは少し異なる部分もあります。

ここでは、その特徴的な部分を中心に、サンプルコードを通して詳しく解説していきます。

このコードでは、抽象クラス内でのプロパティの定義方法と、その利用方法を表しています。

この例では、動物をモデルとした抽象クラスでプロパティを定義し、具体的な動物のクラスでそのプロパティを使用しています。

// 抽象クラス「Animal」を定義
abstract class Animal {
    // 抽象クラス内でプロパティを定義
    name: string;

    // コンストラクターで名前を初期化
    constructor(name: string) {
        this.name = name;
    }

    // 抽象メソッドの定義
    abstract makeSound(): void;
}

// 「Dog」クラスを定義し、抽象クラス「Animal」を継承
class Dog extends Animal {
    // 「makeSound」メソッドを実装
    makeSound(): void {
        console.log(`${this.name}はワンワンと鳴く`);
    }
}

// 「Dog」クラスのインスタンスを作成
const myDog = new Dog("ポチ");
myDog.makeSound(); // 出力結果として、"ポチはワンワンと鳴く"が表示される。

上記のコードを見てわかる通り、抽象クラスAnimal内でnameというプロパティを定義しています。

このプロパティは、具体クラスDogで継承され、利用されます。

そのため、具体クラスでは抽象クラスで定義されたプロパティに自由にアクセスできることが確認できます。

実際に上のコードを実行すると、「ポチはワンワンと鳴く」という結果が表示されます。

このように、抽象クラス内で定義したプロパティを、具体クラスで活用することが可能です。

さて、次に応用例として考えられるのは、抽象クラス内でアクセス修飾子を使用して、プロパティの可視性を制御することです。

たとえば、次のようにして、抽象クラス内でprotectedキーワードを使用してプロパティを定義することも可能です。

abstract class Bird {
    // 「protected」キーワードを使用してプロパティを定義
    protected featherColor: string;

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

    abstract fly(): void;
}

class Sparrow extends Bird {
    fly(): void {
        console.log(`このすずめは${this.featherColor}色の羽で飛ぶ`);
    }
}

const mySparrow = new Sparrow("茶");
mySparrow.fly(); // 出力結果として、「このすずめは茶色の羽で飛ぶ」と表示される。

この例では、featherColorプロパティはprotectedとして定義されているため、Birdクラスを継承するSparrowクラスからはアクセス可能ですが、その他の外部からはアクセスすることはできません。

そのため、抽象クラス内でのプロパティの可視性を制御することにより、より柔軟なクラス設計が可能となります。

●抽象クラスの高度な活用法

TypeScriptの抽象クラスをさらに深く学ぶ上で、いくつかの高度なテクニックやアプローチが存在します。

今回は、これらのテクニックを活用して、更に幅広いシチュエーションでの抽象クラスの使い方を理解していきましょう。

○サンプルコード5:インターフェースとの連携

TypeScriptでは、インターフェースとクラスを組み合わせることで、より柔軟なコード設計が可能です。

抽象クラスとインターフェースを組み合わせることで、複雑なビジネスロジックや複数の役割を持ったクラスの実装が容易となります。

このコードでは、インターフェースと抽象クラスを組み合わせる方法を表しています。

この例では、動物の振る舞いを定義したインターフェースと、抽象クラスを使って具体的な動物の振る舞いを実装しています。

// 動物の振る舞いを定義するインターフェース
interface IAnimalBehavior {
    eat(): void;
    sleep(): void;
}

// 抽象クラスAnimalを作成し、IAnimalBehaviorインターフェースを実装
abstract class Animal implements IAnimalBehavior {
    // 具体的な振る舞いの実装はサブクラスに委ねる
    abstract eat(): void;
    sleep(): void {
        console.log('眠る');
    }
}

// 犬クラス
class Dog extends Animal {
    eat(): void {
        console.log('ドッグフードを食べる');
    }
}

// 犬クラスのインスタンスを作成
const myDog = new Dog();
myDog.eat();  // ドッグフードを食べる
myDog.sleep(); // 眠る

上記の例では、IAnimalBehaviorインターフェースを通じて、動物の基本的な振る舞いを定義しています。

このインターフェースをAnimal抽象クラスで実装し、具体的な動物のクラス(この場合Dogクラス)で振る舞いを具体的に実装しています。

これにより、新しい動物の種類を追加する際も、基本的な振る舞いの定義はインターフェースに従いつつ、具体的な振る舞いの実装はサブクラスで行うことができ、コードの再利用性が向上します。

このコードを実行すると、「ドッグフードを食べる」と「眠る」という結果が得られます。

これにより、我々はTypeScriptでのインターフェースと抽象クラスの連携の基本的な使い方を学ぶことができます。

○サンプルコード6:抽象クラスのミックスイン

TypeScriptの抽象クラスをより深く学ぶ上で、ミックスインというテクニックを避けて通ることはできません。

ミックスインを活用すれば、さまざまなクラスの機能を組み合わせて、新しいクラスを作ることができます。

ここでは、抽象クラスとミックスインを組み合わせて、柔軟なオブジェクト指向プログラムを作成する方法を詳しく解説します。

このコードでは、ミックスインを使って複数のクラスの機能を統合する方法を表しています。

この例では、抽象クラスと具体的なクラスを組み合わせて新しいクラスの機能を展開しています。

// 抽象クラス1: 飛ぶ能力を持つ生物
abstract class Flyable {
    abstract fly(): void;
}

// 抽象クラス2: 歩く能力を持つ生物
abstract class Walkable {
    abstract walk(): void;
}

// ミックスイン関数
function mix<T, U>(C1: T, C2: U): T & U {
    return {...C1.prototype, ...C2.prototype} as T & U;
}

// 具体的なクラス:ペンギン
class Penguin {
    swim() {
        console.log("ペンギンは泳ぐ");
    }
}

// ペンギンに飛ぶ能力と歩く能力をミックスイン
interface Penguin extends Flyable, Walkable {}

mix(Penguin.prototype, Flyable.prototype);
mix(Penguin.prototype, Walkable.prototype);

Penguin.prototype.fly = function() {
    console.log("ペンギンは飛べない");
}

Penguin.prototype.walk = function() {
    console.log("ペンギンは歩く");
}

const penguin = new Penguin();
penguin.swim();
penguin.fly();
penguin.walk();

このサンプルコードを実行すると、ペンギンが「泳ぐ」、「飛べない」、「歩く」という三つの行動を順番に表示することになります。

ミックスインを活用すれば、複数のクラスの機能を一つのクラスに統合することが容易になり、コードの再利用性や拡張性が向上します。

このミックスインの方法を利用すれば、他の多くの動物や生物のクラスも同様に組み合わせて新しいクラスを作ることができるでしょう。

例えば、飛ぶ鳥や、走る動物など、さまざまな動物の特徴を組み合わせて新しいクラスを作る際にも、この方法が有効です。

○サンプルコード7:ジェネリックを利用した抽象クラス

JavaScriptのスーパーセットとして登場したTypeScriptは、多くの先進的な機能を持つことで知られています。

その中でも、「ジェネリック」は非常に強力な機能の1つです。

ジェネリックを利用することで、型の柔軟性を保ちつつも型安全を維持することができます。

さて、ここでは、抽象クラスとジェネリックを組み合わせる方法に焦点を当てます。

ジェネリックを用いることで、抽象クラスをさらにパワフルに、そして柔軟に活用することができます。

このコードでは、ジェネリックを使って抽象クラスを定義する方法を表しています。

この例では、抽象クラス内でジェネリック型を宣言し、その型を用いて抽象メソッドを定義しています。

// 抽象クラスとジェネリックを組み合わせる例
abstract class AbstractData<T> {
    // データを格納するための配列
    protected data: T[] = [];

    // データを追加するための抽象メソッド
    abstract add(item: T): void;

    // データを取得するメソッド
    getData(): T[] {
        return this.data;
    }
}

// 抽象クラスを継承し、ジェネリックを具体的な型で特化
class StringData extends AbstractData<string> {
    // 抽象メソッドの実装
    add(item: string): void {
        this.data.push(item.toUpperCase());
    }
}

上記のサンプルコードでは、AbstractDataという抽象クラスがジェネリック型Tを持っています。

そのため、このクラスを継承する際には、具体的な型を指定する必要があります。

例えば、StringDataクラスでは、ジェネリック型Tstringに特化しています。

StringDataクラスのaddメソッドを利用して文字列データを追加すると、そのデータは大文字に変換されて内部の配列に保存されます。

その結果、追加されたデータを取得すると、すべての文字が大文字となった状態で取得されることになります。

このように、ジェネリックを使った抽象クラスは、型の柔軟性と型安全性を両立しながら、柔軟なクラスの設計が可能となります。

これにより、さまざまなデータ型に対応する共通の処理を持ったクラスを設計することができ、コードの再利用性が向上します。

なお、抽象クラスとジェネリックを組み合わせる際には、ジェネリックの型パラメータを適切に設定することが重要です。

不適切な型パラメータを設定してしまうと、予期しないエラーや不具合の原因となり得ますので注意が必要です。

○サンプルコード8:抽象クラスのアクセス修飾子

TypeScriptでクラスを扱う際、アクセス修飾子を使用してクラス内のメンバー(プロパティやメソッド)の可視性を制御することができます。

そして、抽象クラスでもこれらのアクセス修飾子を活用することで、より堅牢なコードを実現することができます。

このコードでは、抽象クラスにおけるアクセス修飾子の活用方法を紹介します。

この例では、private, protected, public の3種類のアクセス修飾子を使用して、それぞれの挙動を理解するためのサンプルコードを表しています。

// 抽象クラスの定義
abstract class Animal {
    // 公開されたプロパティ(どこからでもアクセス可能)
    public name: string;

    // 保護されたプロパティ(このクラスとサブクラスからアクセス可能)
    protected age: number;

    // 非公開プロパティ(このクラスからのみアクセス可能)
    private isHungry: boolean;

    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
        this.isHungry = true;
    }

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

    // このクラスの中でのみアクセスできるメソッド
    private checkHunger(): boolean {
        return this.isHungry;
    }

    // 公開されたメソッド
    public feed(): void {
        if (this.checkHunger()) {
            console.log(`${this.name}は食事をしました。`);
            this.isHungry = false;
        } else {
            console.log(`${this.name}は食事を必要としていません。`);
        }
    }
}

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

const myDog = new Dog("ポチ", 3);
myDog.makeSound();
myDog.feed();

上記のコードでは、Animalという抽象クラスを定義しています。

このクラスには、nameageisHungryの3つのプロパティがあり、それぞれに異なるアクセス修飾子が付けられています。

具体的には、namepublic修飾子がついており、どこからでもアクセス可能です。

一方、ageprotected修飾子がついており、このクラスとサブクラスからのみアクセスできるようになっています。

最後に、isHungryprivate修飾子がついており、このクラス内からのみアクセスが可能です。

また、Dogというクラスを定義して、Animalクラスを継承しています。

このDogクラスは、抽象メソッドであるmakeSoundを実装しており、具体的な振る舞いを定義しています。

このコードを実行すると、次のような結果が表示されます。

ワンワン!
ポチは食事をしました。

この例から、抽象クラスを使用することで、具体的な実装を子クラスに委ねつつ、アクセス修飾子を活用してデータの保護や可視性を制御することができることがわかります。

○サンプルコード9:静的メソッドと抽象クラス

TypeScriptの抽象クラスは非常に強力で、その特性を利用してさまざまな設計が可能となります。

今回は、抽象クラスにおける静的メソッドの使用法について詳しく解説します。

このコードではTypeScriptの抽象クラスにおいて、静的メソッドを定義し、その使用例を表しています。

この例では、抽象クラス内で静的メソッドを定義し、派生クラスでその静的メソッドを利用して具体的な処理を行います。

// 抽象クラスの定義
abstract class AbstractClass {
    // 抽象プロパティの定義
    abstract prop: string;

    // 静的メソッドの定義
    static showClassName() {
        return "AbstractClass";
    }
}

// 派生クラスの定義
class DerivedClass extends AbstractClass {
    prop = "DerivedClassProperty";

    // 派生クラス独自の静的メソッド
    static showDerivedClassName() {
        return "DerivedClass";
    }
}

// 静的メソッドの呼び出し例
console.log(AbstractClass.showClassName()); // AbstractClassを表示
console.log(DerivedClass.showClassName());  // AbstractClassを表示
console.log(DerivedClass.showDerivedClassName()); // DerivedClassを表示

このコードを実際に動かすと、3つのログがコンソールに表示されます。

最初の2つのログは、抽象クラスと派生クラスの静的メソッドを通してAbstractClassという名前を表示し、最後のログは、派生クラス独自の静的メソッドを使用してDerivedClassという名前を表示しています。

派生クラスで抽象クラスの静的メソッドを呼び出すことができることを覚えておきましょう。

しかし、注意点として、抽象クラスの静的メソッドはオーバーライドすることができません。

そのため、同じ名前の静的メソッドを派生クラスで定義すると、新たな静的メソッドとして認識されます。

応用例として、静的メソッドを使用して、派生クラスごとの設定や初期値を持たせることも可能です。

例えば、各派生クラスに固有の設定を持たせたい場合などに有効です。

abstract class AbstractConfig {
    abstract defaultSetting: string;

    static getDefaultConfig() {
        return "Default Config of Abstract";
    }
}

class DerivedConfigA extends AbstractConfig {
    defaultSetting = "ConfigA";

    static getDefaultConfig() {
        return "Default Config of DerivedA";
    }
}

class DerivedConfigB extends AbstractConfig {
    defaultSetting = "ConfigB";
}

console.log(DerivedConfigA.getDefaultConfig()); // Default Config of DerivedAを表示
console.log(DerivedConfigB.getDefaultConfig()); // Default Config of Abstractを表示

この例では、抽象クラスAbstractConfigとその2つの派生クラスDerivedConfigADerivedConfigBを定義しています。

それぞれのクラスにはデフォルトの設定を返す静的メソッドgetDefaultConfigが定義されており、クラスごとに異なる設定を返すことができます。

○サンプルコード10:抽象クラスの実践的な例

TypeScriptを使用した抽象クラスの実践的な活用には、多様なシチュエーションが存在します。

ここでは、eコマースの製品を扱うオンラインショップの商品クラスを設計するシナリオを想定して、抽象クラスの実践的な活用方法を解説します。

このコードでは、オンラインショップの商品を表す抽象クラスを作成し、それを基にして具体的な商品クラスを実装するコードを紹介しています。

この例では、商品全般の共通の特性や操作を持つ抽象クラスを作り、その上で具体的な商品カテゴリーごとのクラスを実装しています。

// 抽象クラス: 商品
abstract class Product {
    constructor(protected name: string, protected price: number) {}

    // 抽象メソッド: 商品の詳細を取得
    abstract getDetails(): string;

    // 価格を取得するメソッド
    getPrice(): number {
        return this.price;
    }
}

// 本のクラス
class Book extends Product {
    constructor(name: string, price: number, private author: string) {
        super(name, price);
    }

    // 商品の詳細を取得
    getDetails(): string {
        return `${this.name} は、${this.author} 著で、価格は ${this.price} 円です。`;
    }
}

// 家電のクラス
class Appliance extends Product {
    constructor(name: string, price: number, private brand: string) {
        super(name, price);
    }

    // 商品の詳細を取得
    getDetails(): string {
        return `${this.name} は、${this.brand} のブランドで、価格は ${this.price} 円です。`;
    }
}

const book = new Book("TypeScript入門", 3000, "田中太郎");
console.log(book.getDetails());  // 実行すると「TypeScript入門 は、田中太郎 著で、価格は 3000 円です。」と表示されます。

const appliance = new Appliance("エアコン", 25000, "ダイキン");
console.log(appliance.getDetails());  // 実行すると「エアコン は、ダイキン のブランドで、価格は 25000 円です。」と表示されます。

このサンプルコードの中で、Productという抽象クラスを定義しています。

その中で共通のプロパティやメソッドを持ち、さらにgetDetailsという抽象メソッドを定義しています。

このメソッドは、具体的な商品クラスで実装されることを期待しています。

また、具体的な商品クラスとして、BookクラスとApplianceクラスを定義しています。

それぞれのクラスでgetDetailsメソッドを実装しており、その中で具体的な商品の詳細情報を返すようになっています。

上記のコードを実行すると、各商品の詳細情報が適切に表示されます。

このように、抽象クラスを活用することで、異なる種類のオブジェクトに共通のインターフェースや機能を持たせることができます。

これにより、コードの再利用性や拡張性が向上し、より効率的なプログラミングが可能になります。

●抽象クラスの注意点とその対処法

TypeScriptでの抽象クラスを利用する際には、数々の利点がありますが、同時に注意すべき点も存在します。

ここでは、抽象クラスを使用する上での一般的な注意点と、それらの問題を回避または解決するための対処法を紹介していきます。

○具体的な実装を持つことができない

このコードでは抽象クラス内での具体的な実装の有無について考察しています。

この例では抽象メソッドに対して具体的な実装を持たせようとしています。

abstract class AbstractClass {
    abstract abstractMethod(): void;

    // このメソッドは具体的な実装を持つことができません
    abstract implementedMethod() {
        console.log("Implemented in abstract class");
    }
}

このように抽象クラス内で抽象メソッドに具体的な実装を持たせようとすると、コンパイラはエラーを返します。

しかし、非抽象メソッドに対しては具体的な実装を持たせることが許されます。

○抽象クラスは直接インスタンス化できない

このコードでは抽象クラスのインスタンス化について検証しています。

この例では抽象クラスを直接インスタンス化しようとしています。

abstract class AbstractClass {
    abstract sayHello(): void;
}

// 以下のコードはエラーとなります
const instance = new AbstractClass();

上記のように抽象クラスは直接インスタンス化することができません。

これは抽象クラスの性質上、必ず具体クラスによって継承されて使用されることが前提となっているためです。

そのため、次のように具体クラスを作成し、その具体クラスをインスタンス化することで抽象クラスの機能を利用することができます。

abstract class AbstractClass {
    abstract sayHello(): void;
}

class ConcreteClass extends AbstractClass {
    sayHello() {
        console.log("Hello from ConcreteClass!");
    }
}

const instance = new ConcreteClass();
instance.sayHello(); // "Hello from ConcreteClass!" と出力されます

この例ではConcreteClassAbstractClassを継承しており、sayHelloメソッドに具体的な実装を持たせています。

そのため、ConcreteClassのインスタンスを生成し、そのメソッドを呼び出すことで、期待通りの動作が確認できます。

●抽象クラスのカスタマイズと拡張のポイント

TypeScriptの抽象クラスは、その性質上、柔軟な拡張が可能です。

しかし、適切な方法でカスタマイズや拡張を行わないと、コードの保守性や可読性が低下するリスクがあります。

ここでは、抽象クラスをより効果的にカスタマイズや拡張するためのポイントをいくつか紹介します。

○サンプルコード1:カスタマイズされた抽象クラスのメソッド追加

このコードでは、既存の抽象クラスに新しいメソッドを追加するコードを表しています。

この例では、AbstractBaseクラスにprintMessageというメソッドを追加しています。

abstract class AbstractBase {
    abstract sayHello(): string;

    printMessage() {
        console.log(this.sayHello() + "、これはカスタマイズされたメッセージです!");
    }
}

class ConcreteClass extends AbstractBase {
    sayHello(): string {
        return "こんにちは";
    }
}

const example = new ConcreteClass();
example.printMessage();

上記のコードを実行すると、コンソールに「こんにちは、これはカスタマイズされたメッセージです!」と表示されます。

○サンプルコード2:拡張を意識した抽象クラスの定義

このコードでは、将来的な拡張を見越してプロパティやメソッドを定義する抽象クラスの作り方を表しています。

この例では、extensionPointというメソッドを用意して、子クラスでの拡張を容易にしています。

abstract class ExpandableAbstractBase {
    abstract basicMethod(): void;

    // 拡張用のメソッド
    extensionPoint() {
        // デフォルトの処理
    }
}

class ExtendedClass extends ExpandableAbstractBase {
    basicMethod(): void {
        console.log("基本的な処理");
    }

    extensionPoint() {
        console.log("独自の拡張処理");
    }
}

const example2 = new ExtendedClass();
example2.basicMethod();
example2.extensionPoint();

上記のコードを実行すると、コンソールに「基本的な処理」と「独自の拡張処理」と表示されます。

○サンプルコード3:既存のメソッドをオーバーライドして拡張

このコードでは、既存の抽象クラスのメソッドをオーバーライドして機能を拡張する方法を表しています。

この例では、AbstractCalculatorクラスのcalculateメソッドをオーバーライドして、新しい計算機能を追加しています。

abstract class AbstractCalculator {
    abstract calculate(a: number, b: number): number;
}

class AdvancedCalculator extends AbstractCalculator {
    calculate(a: number, b: number): number {
        return a * b;
    }

    // 新しい計算機能の追加
    power(a: number, exponent: number): number {
        return Math.pow(a, exponent);
    }
}

const calculator = new AdvancedCalculator();
console.log(calculator.calculate(3, 4));
console.log(calculator.power(2, 3));

上記のコードを実行すると、まずcalculateメソッドの結果として12が、次にpowerメソッドの結果として8がコンソールに表示されます。

まとめ

TypeScriptの抽象クラスを学ぶ過程で、多くの実用的な知識と技術を得ることができました。

抽象クラスは、オブジェクト指向プログラミングの核心的な部分であり、TypeScriptを使っての開発ではその重要性が増しています。

この記事で紹介した多様なサンプルコードを通じて、抽象クラスの基本から高度な使い方までを網羅的に解説しました。

これらの知識を武器に、TypeScriptの抽象クラスを存分に活用し、より高品質なプログラムを作成していきましょう。

初心者から上級者まで、この記事が皆さんのTypeScriptでの開発に役立つ情報源となれば幸いです。