typescriptというよりjavascriptの話ですけども。
主にクラスを継承しているときに出てくるsuperキーワードで、つまづいたところがあったのでメモ。
superとは
super キーワードは、オブジェクトの親の関数を呼び出すために使用できます。
super.prop および super[expr] 式は、class とオブジェクトリテラルの両方におけるあらゆるメソッド定義で有効です。
・・・オブジェクトにも使えるらしい。知らなかった。
今回は、クラスの継承先が、親のメソッドを呼び出すときに使った時のお話。
コード
class Parent {
constructor() {
console.log('parent constructor');
}
method() {
console.log('parent method');
}
}
class Child extends Parent {
constructor() {
super();
console.log('child constructor');
}
method() {
super.method();
console.log('child method');
}
}
例えばこのようなコードがあったとする。
この時、次のように実行すると、このような結果が得られる。
import { Child } from './main';
const child = new Child();
child.method();
% ts-node exec.ts
parent constructor
child constructor
parent method
child method
おそらくこれは予想通りの結果だと思う。
arrow functionに変えてみる
Parent
クラスのmethod
をarrow functionに変えてみる。
class Parent {
constructor() {
console.log('parent constructor');
}
method = () => {
console.log('parent method');
};
}
class Child extends Parent {
constructor() {
super();
console.log('child constructor');
}
method = () => {
super.method();
console.log('child method');
};
}
一見特に問題なさそうなので、そのまま実行してみる。
% ts-node exec.ts
parent constructor
child constructor
file:///main.ts:18
super.method();
^
TypeError: (intermediate value).method is not a function
エラーが出た。super.method()
が関数ではないと言われている。
原因
"supper" keyword with Arrow function methods · Issue #11575 · microsoft/TypeScript
super
で呼び出せるのは、メソッドではなく、プロパティで定義された関数のみであるとのこと。(多分)
arrow functionはメソッドなので、super
で呼び出せない。
コメント欄指摘より:逆でした。
super
で呼び出せるのは、プロパティではなく、メソッドで定義された関数のみであるとのこと。
arrow functionはプロパティなので、super
で呼び出せない。
解決策
解決策というより回避策な気もするが、いくつかある。
メソッドにする
class Parent {
constructor() {
console.log('parent constructor');
}
method () {
console.log('parent method');
}
}
最初のコード。 このような定義方法で書いた関数はメソッドではなくプロパティという呼び方になるのを今更知る。 メソッドであってる。
もちろん、プロパティとメソッドでは、挙動が異なる時がある(thisとかthisとか・・・)ので、注意が必要。
事前にコピーしておく
class Parent {
constructor() {
console.log('parent constructor');
}
method = () => {
console.log('parent method');
}
}
class Child extends Parent {
private parentMethod = this.method; // 事前にコピーしておく
constructor() {
super();
console.log('child constructor');
}
method = () => {
const parentMethod = this.parentMethod;
parentMethod();
console.log('child method');
}
}
method
を再定義する前にコピーしておくことで、superではないものの、同じようなことができる。
もちろん、method
を定義してからthis.method
をすると、後で定義したものが入るので、method
内で再帰呼び出しからのエラーになる。
参考サイト:アロー関数 - TypeScript Deep Dive 日本語版
その他
プロパティを定義し、専用メソッドを作成し、その中でメソッドを呼び出すという方法もあるようだが、手間の割にメリットが薄いので割愛。
ちなみに、二重継承の場合
Parentクラスを継承したChildクラスを継承したGrandChildクラスを作成する。
その時、
- Parentクラスがメソッドで定義された関数
- Childクラスがプロパティで定義された関数
の場合はどうなるか。
class Parent {
constructor() {
console.log('parent constructor');
}
method() {
console.log('parent method');
}
}
class Child extends Parent {
constructor() {
super();
console.log('child constructor');
}
method = () => {
super.method();
console.log('child method');
};
}
class GrandChild extends Child {
constructor() {
super();
console.log('grandchild constructor');
}
method = () => {
super.method();
console.log('grandchild method');
};
}
import { GrandChild } from './main';
const grandChild = new GrandChild();
grandChild.method();
% ts-node exec.ts
parent constructor
child constructor
grandchild constructor
parent method
grandchild method
結果はこの通り。
Child
のmethod
がプロパティで定義されているので、super
でエラーが出るかと思いきや、
Parent
のmethod
がメソッドで定義されているので、これが呼び出される。
instanceof
演算子がconstructor.prototype
を辿っていくのと同じように、
最寄りの親のメソッドを呼び出すような動作になっていると思う。
終わりに
constructor以外はarrow functionで書けば良いと思っていたが、
意外なところでどちらを使うか考える必要があることを知った。