ゴール:「抽象クラスは“共通の型と骨組みだけを持つ設計図”」だと理解する
抽象クラスは一言でいうと、
「new できないけれど、“こういうクラスであってほしい”という共通ルールを決めるクラス」
です。
- 共通のプロパティ・メソッドは持つ
- でも「ここは必ず子クラスが実装してね」という“穴”も持つ
- その結果、「バラバラな子クラスたちを、同じように扱える」
という状態を作るための仕組みです。
ここを腹で理解できると、
「継承を使う意味」が一段クリアになります。
抽象クラスとは何か:new できない“半分だけ完成したクラス”
abstract class の基本構文
まずは形から。
abstract class Shape {
// 共通のプロパティ
constructor(public color: string) {}
// 共通の具体メソッド
describe(): void {
console.log(`色: ${this.color}`);
}
// 子クラスが必ず実装しなければならない“抽象メソッド”
abstract area(): number;
}
TypeScriptここでのポイントは二つです。
一つ目:abstract class と書かれたクラスは new できません。
// const s = new Shape("red"); // エラー:抽象クラスはインスタンス化できない
TypeScript二つ目:abstract area(): number; のような「中身のないメソッド宣言」を持てること。
これは「このクラスを継承するなら、必ず area() を実装しなさい」という“約束”です。
つまり抽象クラスは、
- 具体的な処理を持つ部分(共通ロジック)
- 子クラスに実装を強制する部分(抽象メソッド)
を両方持つ、「半分だけ完成したクラス」です。
子クラスで“穴”を埋める
抽象クラスを継承する側は、抽象メソッドを必ず実装しなければいけません。
class Circle extends Shape {
constructor(color: string, public radius: number) {
super(color);
}
area(): number {
return this.radius * this.radius * Math.PI;
}
}
class Rectangle extends Shape {
constructor(color: string, public width: number, public height: number) {
super(color);
}
area(): number {
return this.width * this.height;
}
}
const shapes: Shape[] = [
new Circle("red", 10),
new Rectangle("blue", 5, 8),
];
for (const s of shapes) {
s.describe(); // 抽象クラス側の共通メソッド
console.log(s.area()); // 子クラスごとの実装
}
TypeScriptここでの構造はこうです。
Shape:色を持つこと・area()を持つことを“型として”保証するCircle/Rectangle:それぞれの形に応じたarea()の中身を実装する- 呼び出し側:
Shape型として扱うだけで、具体的な形ごとの面積計算を意識しなくてよい
「共通のインターフェース+一部だけ子クラスに任せる」
これが抽象クラスの本質です。
抽象クラスとインターフェースの違い
ざっくり言うと「中身を持てるかどうか」
インターフェースとの違いがよく聞かれるポイントなので、
初心者向けに一番大事なところだけ押さえます。
インターフェース:
interface Notifier {
send(message: string): void;
}
TypeScript- 中身(実装)は持てない
- 「こういうメソッドを持っていてね」という“形”だけを決める
抽象クラス:
abstract class Notifier {
abstract send(message: string): void;
log(message: string): void {
console.log("送信ログ:", message);
}
}
TypeScript- 抽象メソッド(中身なし)も持てる
- 具体メソッド(中身あり)も持てる
- 共通ロジックを“ここに書いて共有”できる
つまり、
「共通の処理もまとめたい」なら抽象クラス
「形だけ決めたい」ならインターフェース
という使い分けが基本になります。
抽象クラス設計のコア:何を“共通”にして、何を“抽象”にするか
共通ロジックは抽象クラスに閉じ込める
例えば、「通知を送る」仕組みを考えます。
abstract class Notifier {
constructor(public sender: string) {}
// 共通の前処理
protected format(message: string): string {
return `[from: ${this.sender}] ${message}`;
}
// 子クラスに実装を強制する“抽象メソッド”
abstract send(message: string): void;
}
class EmailNotifier extends Notifier {
send(message: string): void {
const formatted = this.format(message); // 共通ロジックを再利用
console.log("メール送信:", formatted);
}
}
class LineNotifier extends Notifier {
send(message: string): void {
const formatted = this.format(message);
console.log("LINE送信:", formatted);
}
}
TypeScriptここでの設計のポイントは、
- 「送信前にメッセージを整形する」という共通ロジックは抽象クラスに書く
- 「どう送るか(メールか LINE か)」という具体ロジックは子クラスに任せる
- 抽象クラスは
sendの“存在”を強制しつつ、formatという“便利機能”も提供している
ということです。
「全員が必ず持つべきメソッド」と
「全員が共通で使える便利メソッド」
この2つをセットで提供できるのが、抽象クラスの強みです。
抽象メソッドは「ここは必ず子クラスが決めて」と宣言する場所
抽象メソッドを書くときは、
「ここはこのクラスでは決めきらない。
でも、継承するなら絶対に実装してほしい」という場所です。
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()抽象メソッドとして子クラスに任せる - 呼び出し側は
job.execute()だけ呼べばよい
という構造になっています。
「流れ(テンプレート)は親が決める。
中身の一部だけ子が埋める」
このパターンは実務でかなりよく出てきます。
抽象クラスを使うか迷ったときの判断軸
「new されるべき“完成品”か?それとも“土台”か?」
クラスを書いていて、
「これは直接 new されるべきか?」と一瞬考えてみてください。
もし、
- これ単体では意味が弱い
- 必ず何かの“具体クラス”に継承される前提
- 共通ロジックと共通インターフェースをまとめたい
という感覚があるなら、それは抽象クラス候補です。
逆に、
- そのクラス自体を new して普通に使う
- 継承されるかどうかはおまけ程度
なら、普通のクラスで十分です。
「インターフェース+ユーティリティ関数」で済むなら、そっちも検討する
抽象クラスは強力ですが、
「継承」という仕組みを持ち込むぶん、依存関係が強くなります。
もし、
- 共通ロジックは「ただの関数」として外に出せる
- クラス同士を強く結びつけたくない
という状況なら、
interface Shape {
area(): number;
}
function describeShape(shape: Shape): void {
console.log("面積:", shape.area());
}
TypeScriptのように、
インターフェース+関数で表現するほうがシンプルなことも多いです。
「共通ロジックを“継承”で共有したいのか?
それとも“関数”として共有したいのか?」
ここを意識して選べるようになると、
抽象クラスの出番が“本当に必要なところ”に絞られていきます。
まとめ:抽象クラスの設計を自分の言葉で整理すると
最後に、あなた自身の言葉でこう整理してみてください。
抽象クラスは、
- new できない「半分だけ完成したクラス」
- 共通のプロパティ・具体メソッドを持てる
- 抽象メソッドで「子クラスが必ず実装すべき穴」を宣言できる
- 「共通の流れは親が決めて、一部だけ子に任せる」設計に向いている
そして設計のポイントは、
- 「ここは全サブクラスで共通にしたい」ロジックを抽象クラスに集約する
- 「ここは各サブクラスごとに違うべき」部分を抽象メソッドにする
- その結果、「親型(抽象クラス)としてまとめて扱える」ようにする
今のあなたのコードの中で、
「似たようなクラスがいくつもあって、
同じような前処理・後処理を書き散らしている場所」があれば、
そこを抽象クラス+抽象メソッドに整理できないか考えてみてください。
一度そこがハマると、
“コピペだらけのクラス群”が、“共通ルールを持ったきれいな階層”に変わっていきます。
