TypeScript | 関数・クラス・ジェネリクス:クラス設計 – メソッドオーバーライド

TypeScript TypeScript
スポンサーリンク

ゴール:「同じメソッド名でも“クラスごとに振る舞いを変える”感覚をつかむ」

メソッドオーバーライドは一言でいうと、

「親クラスと同じ名前・同じ型のメソッドを、子クラス側で“書き直す”こと」

です。

これができると、

move() と書いているのに、
犬なら犬の動き、鳥なら鳥の動きになる」

みたいな“気持ちいい多態性”を作れるようになります。


メソッドオーバーライドの基本イメージ

まずは「親クラスのメソッドをそのまま使う」パターン

最初に、オーバーライドしない場合から。

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

  speak(): void {
    console.log(`${this.name} は何か鳴いた`);
  }
}

class Dog extends Animal {
  // 何も書かないと、speak は親クラスのものをそのまま使う
}

const a = new Animal("なぞの生き物");
const d = new Dog("ポチ");

a.speak(); // なぞの生き物 は何か鳴いた
d.speak(); // ポチ は何か鳴いた(Animal の実装)
TypeScript

DogAnimalextends しているので、
Animalspeak() をそのまま使えます。

「同じ名前のメソッドを子クラスで書き直す」のがオーバーライド

ここで、「犬はちゃんと“ワン”と鳴いてほしい」とします。

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

  speak(): void {
    console.log(`${this.name} は何か鳴いた`);
  }
}

class Dog extends Animal {
  speak(): void {
    console.log(`${this.name} がワンと鳴いた`);
  }
}

const a = new Animal("なぞの生き物");
const d = new Dog("ポチ");

a.speak(); // なぞの生き物 は何か鳴いた
d.speak(); // ポチ がワンと鳴いた(Dog の実装)
TypeScript

ここでやっていることが「メソッドオーバーライド」です。

ポイントは、

  • メソッド名が同じ(speak
  • 引数・戻り値の型も同じ(ここでは (): void
  • でも中身の実装はクラスごとに違う

という状態になっていることです。


型の観点から見るメソッドオーバーライド

親クラスの型を“壊さない”のが大前提

TypeScript 的には、
子クラスのメソッドは「親クラスのメソッドと互換性のある型」でなければいけません。

class Animal {
  speak(message: string): void {
    console.log(`Animal: ${message}`);
  }
}

class Dog extends Animal {
  // OK:引数・戻り値の型が同じ
  speak(message: string): void {
    console.log(`Dog: ${message}`);
  }
}
TypeScript

もし、こんなふうに型を変えようとすると危険です。

class Dog extends Animal {
  // NG 例(イメージ):引数の型を変えたり、戻り値の型を変えたり
  // speak(count: number): number { ... }
}
TypeScript

なぜかというと、
DogAnimal として扱ったときに破綻するからです。

const animal: Animal = new Dog("ポチ");
// animal.speak("こんにちは"); // Animal 的には string を渡す前提
TypeScript

だからこそ、

「オーバーライドするときは、親クラスと同じシグネチャ(引数・戻り値の型)にする」

というのが基本ルールになります。

「型としては同じ」「中身だけ違う」が理想

メソッドオーバーライドの理想形は、

  • 型:親も子も同じ(呼び出し側から見た“約束”は変わらない)
  • 実装:クラスごとに違う(実際の振る舞いだけ変わる)

という状態です。

class Animal {
  move(): void {
    console.log("なにかが動いた");
  }
}

class Dog extends Animal {
  move(): void {
    console.log("犬が走った");
  }
}

class Bird extends Animal {
  move(): void {
    console.log("鳥が飛んだ");
  }
}

const animals: Animal[] = [
  new Animal(),
  new Dog(),
  new Bird(),
];

for (const a of animals) {
  a.move(); // 型的には全部 Animal の move(): void
}
TypeScript

呼び出し側は「Animalmove() を呼んでいる」つもりですが、
実際にはそれぞれのクラスの move() が呼ばれます。

これが「型としては同じ」「中身だけ違う」という状態です。


super と組み合わせたオーバーライド

親の処理を“ベースにして”少しだけ変えたいとき

オーバーライドするとき、
「親の処理を完全に捨てる」のではなく、
「親の処理を呼んだうえで、前後に何か足したい」こともよくあります。

class Logger {
  log(message: string): void {
    console.log(`[LOG] ${message}`);
  }
}

class TimestampLogger extends Logger {
  log(message: string): void {
    const withTime = `${new Date().toISOString()} ${message}`;
    super.log(withTime); // 親クラスの log を呼ぶ
  }
}

const logger = new TimestampLogger();
logger.log("Hello");
// [LOG] 2026-02-07T... Hello
TypeScript

ここでのポイントは、

  • TimestampLoggerlog をオーバーライドしている
  • でも「実際の出力」は親クラスの log に任せている
  • 子クラスは「ログメッセージに時刻を足す」という“差分”だけを担当している

という設計になっていることです。

「親クラスの責務は尊重しつつ、子クラスで差分だけ書く」
そのための道具として、オーバーライド+super はとても相性がいいです。


実務でのメソッドオーバーライドの使いどころ

「共通インターフェース+クラスごとの振る舞い」のパターン

例えば、通知を送るクラスを考えます。

abstract class Notifier {
  abstract send(message: string): void;
}

class EmailNotifier extends Notifier {
  send(message: string): void {
    console.log(`メール送信: ${message}`);
  }
}

class LineNotifier extends Notifier {
  send(message: string): void {
    console.log(`LINE送信: ${message}`);
  }
}

function notifyAll(notifiers: Notifier[], message: string) {
  for (const n of notifiers) {
    n.send(message); // 型的には Notifier の send
  }
}

const list: Notifier[] = [
  new EmailNotifier(),
  new LineNotifier(),
];

notifyAll(list, "こんにちは");
TypeScript

ここでは、

  • 親クラス(Notifier)が「send(message: string): void という約束」を定義
  • 子クラス(EmailNotifier, LineNotifier)が、そのメソッドをオーバーライドして具体的な実装を書く
  • 呼び出し側は「Notifier として扱う」だけで、実際に何が起きるかはクラスごとに違う

という構造になっています。

「同じメソッド名で、クラスごとに違う振る舞いをさせたい」
というとき、メソッドオーバーライドは本領発揮します。


まとめ:メソッドオーバーライドを自分の言葉で整理すると

最後に、あなた自身の言葉でこう整理してみてください。

メソッドオーバーライドとは、

  • 親クラスと同じ名前・同じ型のメソッドを、子クラスで“書き直す”こと
  • 型としての約束(引数・戻り値)は変えずに、中身の実装だけ変えること
  • 呼び出し側から見ると「同じメソッド」を呼んでいるのに、
    実際にはクラスごとに違う動きをしてくれる仕組み

そして設計のポイントは、

  • 「A は B の一種」と言える継承関係の中で使う
  • 親の責務を壊さず、「差分だけ」を子クラスに書く
  • 必要なら super.xxx() で親の実装を呼び出しつつ拡張する

今のあなたのコードの中で、
「if 文や switch で種類ごとに分岐している処理」があれば、
それを「クラスごとのオーバーライド」に置き換えられないか考えてみてください。

その瞬間から、
“条件分岐だらけの関数”が、“クラスごとに責務が分かれた設計”に変わっていきます。

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