ゴール:「super は“親クラス側の自分”を呼び出すキーワードだ」と腹で理解する
super は、継承しているときだけ出てくる特別なキーワードです。
一言でいうと、
「親クラス側の自分を呼び出すためのスイッチ」
です。
コンストラクタでの super(...) と、
メソッドの中での super.xxx()、
この2つの役割を分けてイメージできるようになると、一気にスッキリします。
コンストラクタでの super:親クラスの初期化を“必ず”呼ぶ
なぜ子クラスのコンストラクタで super(…) が必要なのか
継承ありのクラスで、子クラスにコンストラクタを書くときは、
ほぼ必ずこうなります。
class Animal {
name: string;
constructor(name: string) {
console.log("Animal constructor");
this.name = name;
}
}
class Dog extends Animal {
constructor(name: string) {
console.log("Dog constructor start");
super(name); // ここが super
console.log("Dog constructor end");
}
}
const d = new Dog("ポチ");
TypeScriptここでの超重要ポイントは、
「子クラスのコンストラクタでは、super(...) を一番最初に呼ばないといけない」
というルールです。
super(...) は、
- 親クラスのコンストラクタを呼び出す
- 親クラス側の初期化(共通部分のセットアップ)を完了させる
- その結果として、
thisを安全に使えるようにする
という役割を持っています。
もし super(...) より前に this を触ろうとすると、TypeScript/JavaScript は怒ります。
class Dog extends Animal {
constructor(name: string) {
// console.log(this.name); // ここはエラー
super(name);
console.log(this.name); // ここなら OK
}
}
TypeScript「親クラスの準備が終わる前に、子クラスが勝手に this を触るのは危険」
だからこそ、super(...) が“最初の一手”として必須なんです。
親クラスに渡すべき情報を super(…) で渡す
親クラスがコンストラクタで何かを受け取るなら、
子クラスはそれを super(...) でちゃんと渡してあげる必要があります。
class Animal {
constructor(public name: string) {}
}
class Dog extends Animal {
constructor(name: string, public kind: string) {
super(name); // name は親に渡す
}
}
const d = new Dog("ポチ", "柴犬");
console.log(d.name); // 親クラス側のプロパティ
console.log(d.kind); // 子クラス側のプロパティ
TypeScriptここでの役割分担はこうです。
- 親クラス:全ての動物が共通で持つ情報(name)を初期化する
- 子クラス:犬として固有の情報(kind)を初期化する
super(name) は、「親クラスが担当する初期化に必要な材料を渡す」ための呼び出しです。
「共通部分の初期化は親に任せる」という設計にすると、
クラス階層がきれいに保てます。
メソッド内の super.xxx():親クラスの実装を“ベースにして”上書きする
メソッドをオーバーライドしつつ、親の処理も使いたいとき
super はコンストラクタだけでなく、メソッドの中でも使えます。
class Animal {
constructor(public name: string) {}
move() {
console.log(`${this.name} がトコトコ歩いた`);
}
}
class Dog extends Animal {
move() {
console.log("前足を出す");
super.move(); // 親クラスの move を呼ぶ
console.log("しっぽを振る");
}
}
const d = new Dog("ポチ");
d.move();
TypeScriptこのときの出力イメージは、
前足を出す
ポチ がトコトコ歩いた
しっぽを振る
のようになります。
ここでのポイントは、
Dogはmove()をオーバーライドしている(自分の実装を持っている)- その中で
super.move()を呼ぶことで、「親クラスの move の処理」も挟み込んでいる
ということです。
「基本の動きは親クラスに任せつつ、前後に“犬ならでは”の動きを足したい」
そんなときに super.move() が効いてきます。
super.xxx() は「親クラス版のメソッド」を指す
super.move() は、「親クラスに定義されている move メソッド」を呼びます。
もし親クラスにそのメソッドがなければ、当然エラーです。
class A {
foo() {
console.log("A.foo");
}
}
class B extends A {
foo() {
super.foo(); // A.foo を呼ぶ
console.log("B.foo");
}
}
TypeScriptここでのイメージは、
this.foo()→ 「今のクラス(B)の foo」super.foo()→ 「一つ上のクラス(A)の foo」
という感じです。
「親の実装をベースに、子で少しだけ振る舞いを変えたい」
というときに、super はとても自然な選択になります。
super を使うときに意識してほしい設計の感覚
「親が“責任を持つべき処理”は super に任せる」
コンストラクタでもメソッドでも、super を使うときに大事なのは、
「ここは親クラスの責任だよね?」
という感覚です。
コンストラクタなら、
- 親クラスが共通プロパティを初期化する責任を持つ
- 子クラスは、自分固有のプロパティだけを初期化する
メソッドなら、
- 親クラスが「基本的な振る舞い」を定義する
- 子クラスは、それを拡張したり、前後に処理を足したりする
という役割分担になります。
super は、
「親に任せるべきところを、ちゃんと親に任せる」
ための明示的な呼び出しだと捉えてください。
「全部子クラスでやる」は継承のメリットを殺す
もし子クラスのコンストラクタで、
親クラスの初期化ロジックをコピペしてしまったらどうなるか。
class Animal {
constructor(public name: string) {}
}
class Dog extends Animal {
constructor(name: string) {
// super(name) を呼ばずに、自分で name を持とうとする…みたいな設計
// これはもう「Animal を継承している意味」が薄くなる
super(name);
}
}
TypeScript親クラスが変わったときに、
子クラス側のコピペも全部直さないといけなくなります。
super をちゃんと使うことで、
- 共通部分は親に一元化
- 子は「違い」だけを書く
という継承のメリットを最大限に活かせます。
まとめ:super の役割を自分の言葉で整理すると
最後に、あなた自身の言葉でこう言い換えてみてください。
コンストラクタでの super(...) は、
「親クラスのコンストラクタを呼び出して、
共通部分の初期化を完了させるためのもの。
これを一番最初に呼ばないと、this は安全に使えない。」
メソッド内の super.xxx() は、
「同じ名前のメソッドについて、
“親クラス版の実装”を呼び出すためのもの。
親の処理をベースに、子で振る舞いを足したり変えたりできる。」
今あなたが書いている継承クラスを一つ選んで、
「このコンストラクタで super は何をしている?」
「このメソッドで super.xxx() を呼ぶと、どのクラスのどの処理が動く?」
と、実際に console.log を仕込んで動きを追ってみてください。
一度「目で見て」動きを理解すると、super は単なるキーワードではなく、
“親にバトンを渡すための明確な合図”として見えてきます。
