ゴール:「同じメソッド名でも“クラスごとに振る舞いを変える”感覚をつかむ」
メソッドオーバーライドは一言でいうと、
「親クラスと同じ名前・同じ型のメソッドを、子クラス側で“書き直す”こと」
です。
これができると、
「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 の実装)
TypeScriptDog は Animal を extends しているので、Animal の speak() をそのまま使えます。
「同じ名前のメソッドを子クラスで書き直す」のがオーバーライド
ここで、「犬はちゃんと“ワン”と鳴いてほしい」とします。
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なぜかというと、Dog を Animal として扱ったときに破綻するからです。
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呼び出し側は「Animal の move() を呼んでいる」つもりですが、
実際にはそれぞれのクラスの 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ここでのポイントは、
TimestampLoggerはlogをオーバーライドしている- でも「実際の出力」は親クラスの
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 で種類ごとに分岐している処理」があれば、
それを「クラスごとのオーバーライド」に置き換えられないか考えてみてください。
その瞬間から、
“条件分岐だらけの関数”が、“クラスごとに責務が分かれた設計”に変わっていきます。
