TypeScript | 関数・クラス・ジェネリクス:クラス設計 – superの役割

TypeScript TypeScript
スポンサーリンク

ゴール:「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

このときの出力イメージは、

前足を出す
ポチ がトコトコ歩いた
しっぽを振る

のようになります。

ここでのポイントは、

  • Dogmove() をオーバーライドしている(自分の実装を持っている)
  • その中で 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 は単なるキーワードではなく、
“親にバトンを渡すための明確な合図”として見えてきます。

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