【typescript】superはarrow functionに使えない

typescriptというよりjavascriptの話ですけども。
主にクラスを継承しているときに出てくるsuperキーワードで、つまづいたところがあったのでメモ。

# superとは

> super キーワードは、オブジェクトの親の関数を呼び出すために使用できます。
> super.prop および super[expr] 式は、class とオブジェクトリテラルの両方におけるあらゆるメソッド定義で有効です。

[super - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/super)より

・・・オブジェクトにも使えるらしい。知らなかった。
今回は、クラスの継承先が、親のメソッドを呼び出すときに使った時のお話。

# コード

```typescript:main.ts
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');
  }
}
```

例えばこのようなコードがあったとする。
この時、次のように実行すると、このような結果が得られる。

```typescript:exec.ts
import { Child } from './main';
const child = new Child();
child.method();
```

```zsh
% ts-node exec.ts
parent constructor
child constructor
parent method
child method
```

おそらくこれは予想通りの結果だと思う。

# arrow functionに変えてみる

`Parent`クラスの`method`をarrow functionに変えてみる。

```typescript:main.ts
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');
  };
}
```

一見特に問題なさそうなので、そのまま実行してみる。

```zsh
% 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](https://github.com/microsoft/TypeScript/issues/11575)

~~`super`で呼び出せるのは、メソッドではなく、プロパティで定義された関数のみであるとのこと。(多分)~~
~~arrow functionはメソッドなので、`super`で呼び出せない。~~

コメント欄指摘より：逆でした。

`super`で呼び出せるのは、プロパティではなく、メソッドで定義された関数のみであるとのこと。
arrow functionはプロパティなので、`super`で呼び出せない。


# 解決策

解決策というより回避策な気もするが、いくつかある。

## メソッドにする

```typescript:main.ts
class Parent {
  constructor() {
    console.log('parent constructor');
  }

  method () {
    console.log('parent method');
  }
}
```

最初のコード。 ~~このような定義方法で書いた関数はメソッドではなくプロパティという呼び方になるのを今更知る。~~ メソッドであってる。
もちろん、プロパティとメソッドでは、挙動が異なる時がある(thisとかthisとか・・・)ので、注意が必要。

## 事前にコピーしておく

```typescript:main.ts
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 日本語版](https://typescript-jp.gitbook.io/deep-dive/future-javascript/arrow-functions#tiparto)

## その他

プロパティを定義し、専用メソッドを作成し、その中でメソッドを呼び出すという方法もあるようだが、手間の割にメリットが薄いので割愛。

[Typescript: How to call method defined with arrow function in base class using super keyword in child class? - Stack Overflow](https://stackoverflow.com/questions/23218959/typescript-how-to-call-method-defined-with-arrow-function-in-base-class-using-s)

# ちなみに、二重継承の場合

Parentクラスを継承したChildクラスを継承したGrandChildクラスを作成する。
その時、
- Parentクラスがメソッドで定義された関数
- Childクラスがプロパティで定義された関数
の場合はどうなるか。

```typescript:main.ts
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');
  };
}
```

```typescript:exec.ts
import { GrandChild } from './main';
const grandChild = new GrandChild();
grandChild.method();
```

```zsh
% 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で書けば良いと思っていたが、
意外なところでどちらを使うか考える必要があることを知った。