TypeScript | 関数・クラス・ジェネリクス:クラス設計 – 抽象メソッドの定義

TypeScript TypeScript
スポンサーリンク

ゴール:「抽象メソッド=“ここは必ず子クラスが実装して”という穴あきメソッド」と理解する

抽象メソッドは一言でいうと、

「このメソッドは“存在すること”だけ決めておいて、中身は子クラスに必ず書かせる仕組み」

です。

「共通のルールは決めたい。でも具体的な中身は各クラスごとに違う」
そんなときに、抽象メソッドが本領発揮します。


抽象メソッドの基本構文とルール

abstract class の中でしか使えない

抽象メソッドは、必ず抽象クラスの中で定義します。

abstract class Shape {
  // 抽象メソッド(中身なし)
  abstract area(): number;
}
TypeScript

ここで重要なのは次の2点です。

  • abstract キーワードが付いている
  • 中身(ブロック { ... })がない(セミコロンで終わる)

つまり、

abstract area(): number;
TypeScript

は、

area という名前で、引数なし・戻り値 number のメソッドを
“必ず持っていなければならない”」

という“約束だけ”を表しています。

抽象メソッドを持つクラスは new できない

抽象メソッドを1つでも持っているクラスは、abstract class でなければなりません。

abstract class Shape {
  abstract area(): number;
}

// const s = new Shape(); // エラー:抽象クラスはインスタンス化できない
TypeScript

このクラスは「設計図」であって「完成品」ではない、というイメージを持ってください。
“穴”(抽象メソッド)が埋まっていないので、そのままでは使えないのです。


子クラスで抽象メソッドを実装する

「穴を埋めないとコンパイルエラーになる」

抽象クラスを継承した子クラスは、
抽象メソッドを必ず実装しなければいけません。

abstract class Shape {
  abstract area(): number;
}

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

  area(): number {
    return this.radius * this.radius * Math.PI;
  }
}
TypeScript

ここでのポイントは、

  • 親クラスの area(): number
    子クラスの area(): number の「シグネチャ(引数・戻り値の型)」が一致していること
  • 子クラス側では、abstract を付けずに“中身を実装する”こと

です。

もし Circlearea() を実装しなかったら、TypeScript に怒られます。

class BadCircle extends Shape {
  // area を実装していないのでエラー
}
TypeScript

抽象メソッドは、

「このクラスを継承するなら、ここは絶対にサボらないで実装して」

という強制力を持った“契約”だと思ってください。

型は「親が決める」「子は従う」

抽象メソッドの型(引数・戻り値)は、親クラス側で決めます。

abstract class Job {
  abstract run(times: number): boolean;
}
TypeScript

この場合、子クラスは必ず同じシグネチャで実装しなければなりません。

class BackupJob extends Job {
  run(times: number): boolean {
    console.log(`${times} 回バックアップを実行`);
    return true;
  }
}
TypeScript

ここで大事なのは、

  • 呼び出し側は「Job 型として run(times: number): boolean を呼べる」と信じている
  • だからこそ、子クラスもその約束を破ってはいけない

という点です。

抽象メソッドは、
「子クラスたちが共有する“インターフェース(型の約束)”を、親クラス側で宣言する場所」
だと捉えると、設計の意味が見えてきます。


抽象メソッドと共通ロジックの組み合わせ

「流れは親が決める・中身は子が決める」というパターン

抽象メソッドが一番輝くのは、
「処理の流れは共通だけど、肝心な1ステップだけクラスごとに違う」
という場面です。

abstract class Job {
  constructor(public name: string) {}

  // 子クラスに実装を強制する“肝心な処理”
  abstract run(): void;

  // 共通の実行フロー
  execute(): void {
    console.log(`ジョブ開始: ${this.name}`);
    this.run(); // ここで子クラスの実装が呼ばれる
    console.log(`ジョブ終了: ${this.name}`);
  }
}

class BackupJob extends Job {
  run(): void {
    console.log("バックアップを実行");
  }
}

class CleanupJob extends Job {
  run(): void {
    console.log("不要ファイルを削除");
  }
}

const jobs: Job[] = [
  new BackupJob("バックアップ"),
  new CleanupJob("クリーンアップ"),
];

for (const job of jobs) {
  job.execute();
}
TypeScript

ここでの設計のキモは、

  • execute() の「前後のログ出力」は全ジョブで共通 → 抽象クラスに書く
  • 真ん中の「何をするか」はジョブごとに違う → 抽象メソッド run() にして子クラスに任せる

という分担です。

抽象メソッドは、
「共通フローの中にある“差し替えポイント”」
を表現するための道具だと考えると、使いどころが見えてきます。


抽象メソッドを設計するときに考えるべきこと

「ここは全サブクラスが必ず持っていてほしいか?」

抽象メソッドにするかどうか迷ったら、
自分にこう問いかけてみてください。

「このメソッドは、この抽象クラスを継承する“全ての子クラス”が
必ず持っていてほしいものか?」

例えば、Shape なら area() はそうでしょう。

  • 円でも
  • 四角形でも
  • 三角形でも

「面積を計算できる」という前提があるからこそ、
Shape という抽象クラスが意味を持ちます。

逆に、

  • 一部の子クラスにしか関係ない
  • なくても成立する子クラスがある

というメソッドは、抽象メソッドにすべきではありません。

「型の約束を“親が握る”」という意識

抽象メソッドの型は、親クラスが決めます。

abstract class Notifier {
  abstract send(message: string): Promise<void>;
}
TypeScript

この時点で、

  • どの具体的な Notifier(メール・LINE・Slack…)であっても
  • sendmessage: string を受け取り
  • Promise<void> を返す

という約束が固まります。

呼び出し側は、
Notifier 型さえ渡されれば、await notifier.send("...") と書ける」
と信じてコードを書けます。

抽象メソッドは、
「バラバラな実装を、同じ“呼び出し方”で扱えるようにするための型の土台」
だと意識して設計してみてください。


まとめ:抽象メソッドを自分の言葉で整理すると

最後に、あなた自身の言葉でこうまとめてみてください。

抽象メソッドは、

  • 抽象クラスの中でだけ宣言できる「中身のないメソッド」
  • 「このクラスを継承するなら、このメソッドを必ず実装してね」という“契約”
  • 型(引数・戻り値)は親が決めて、子クラスはそれに従って中身を書く
  • 共通フローの中の「差し替えポイント」を表現するのに向いている

今のあなたのコードの中で、

「毎回同じ前処理・後処理を書いていて、
真ん中だけ if/switch で分岐している処理」

があれば、それを

  • 抽象クラスの共通メソッド(前後処理)
  • 抽象メソッド(真ん中の差し替え部分)

に分解できないか考えてみてください。

その瞬間から、
“条件分岐だらけの関数”が、“抽象クラス+抽象メソッドで整理された設計”に変わっていきます。

タイトルとURLをコピーしました