ゴール:「抽象メソッド=“ここは必ず子クラスが実装して”という穴あきメソッド」と理解する
抽象メソッドは一言でいうと、
「このメソッドは“存在すること”だけ決めておいて、中身は子クラスに必ず書かせる仕組み」
です。
「共通のルールは決めたい。でも具体的な中身は各クラスごとに違う」
そんなときに、抽象メソッドが本領発揮します。
抽象メソッドの基本構文とルール
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を付けずに“中身を実装する”こと
です。
もし Circle が area() を実装しなかったら、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…)であっても
sendはmessage: stringを受け取りPromise<void>を返す
という約束が固まります。
呼び出し側は、
「Notifier 型さえ渡されれば、await notifier.send("...") と書ける」
と信じてコードを書けます。
抽象メソッドは、
「バラバラな実装を、同じ“呼び出し方”で扱えるようにするための型の土台」
だと意識して設計してみてください。
まとめ:抽象メソッドを自分の言葉で整理すると
最後に、あなた自身の言葉でこうまとめてみてください。
抽象メソッドは、
- 抽象クラスの中でだけ宣言できる「中身のないメソッド」
- 「このクラスを継承するなら、このメソッドを必ず実装してね」という“契約”
- 型(引数・戻り値)は親が決めて、子クラスはそれに従って中身を書く
- 共通フローの中の「差し替えポイント」を表現するのに向いている
今のあなたのコードの中で、
「毎回同じ前処理・後処理を書いていて、
真ん中だけ if/switch で分岐している処理」
があれば、それを
- 抽象クラスの共通メソッド(前後処理)
- 抽象メソッド(真ん中の差し替え部分)
に分解できないか考えてみてください。
その瞬間から、
“条件分岐だらけの関数”が、“抽象クラス+抽象メソッドで整理された設計”に変わっていきます。
