JavaScriptでMixinを作る方法

ハイサイ！オースティンやいびーん

# 概要

JavaScriptでクラス継承の裏技をしてくれるMixinの記述法を紹介します。

## Mixinとは

JavaScriptではES6以降、オブジェクト指向のクラス構文が可能になりました。これによって色々と関数のプロパティを継承させる方法が楽になったのですが、まだ欠点があります。

その欠点とは、一連のクラス継承じゃないと、いくつかの性質、プロパティ、そしてメソッドを含められないことです。

PHPでは、Traitを使うと、簡単に再利用可能なロジックを記述できます：

```php
trait ezcReflectionReturnInfo {
    function getReturnType() { /*1*/ }
    function getReturnDescription() { /*2*/ }
}

class ezcReflectionMethod extends ReflectionMethod {
    use ezcReflectionReturnInfo;
    /* ... */
}
```
残念ながら、JavaScriptではこう簡単にいかないのです。Traitがないので、ランタイム時にクラス継承を操ってくれる**Mixin**という手法でTraitに似たような書き方ができるようになります。

JavaScriptでMixinを使うことはやや上級者向けにしてもとても一般的な習慣なので、決してハックではありません。しかし、ややこしいところがあります。特にTypeScriptは面倒です。

# JavaScriptでMixinを使用する方法

JavaScriptだけならとても簡単です。

例えば、RxJSをWeb Componentで使っていて、Web ComponentがDOMから消えた時に`unsubscribe`をしてメモリリークを防ぎたい時に、よく以下のような書き方をします。

```javascript
const {Subject, fromEvent} = rxjs;
// OR, importmapを使っていれば
import {Subject, fromEvent, takeUntil} from "rxjs";

class MyObserverComponent extends HTMLElement {
  teardown$ = /** @type {import('rxjs').Subject<void>} */ (new Subject());

  connectedCallback() {
    fromEvent(this, "click")
      .pipe(takeUntil(this.teardown$))
      .subscribe(e => {
        // Logic
      });
  }

  disconnectedCallback() {
    if (typeof super.disconnectedCallback === "function") {
      super.disconnectedCallback();
    }
  }
}

customElements.define("my-observer-component", MyObserverComponent);
```

似たような`teardown$`を持ったWeb Componentをたくさん書くので、Mixinを使ってクラス継承を気にせずに`teardown$`を持たせたいのです。

```javascript
const TeardownableComponent = superClass =>
  class extends superClass {
    teardown$ = /** @type {import('rxjs').Subject<void>} */ (new Subject());

    disconnectedCallback() {
      if (typeof super.disconnectedCallback === "function") {
        super.disconnectedCallback();
      }
    }
  };
```

こうすると、上記のクラス定義を以下のように書き直せば、`teardown$`を継承できるようになります。

```javascript
class MyObserverComponent extends TeardownableComponent(HTMLElement) {
  connectedCallback() {
    fromEvent(this, "click")
      .pipe(takeUntil(this.teardown$))
      .subscribe(e => {
        // Logic
      });
  }
}
```

JavaScriptだけなら、やはりシンプルです！

# TypeScriptを使う場合

TypeScriptだと、ややこしくなります。まず、`Constructor`の型を定義する必要があります。

引用：[TypeScriptのドキュメント](https://www.typescriptlang.org/docs/handbook/mixins.html)を参考にしています。

```typescript
type Constructor<T = {}> = new (...args: any[]) => T;
```

次に、この型を使ってMixinのロジックを書きますが、`disconnectedCallback`辺りで問題になります。

```typescript
export function TeardownableComponent<
  T extends Constructor<WebComponentWithPossibleConnectedCallback>,
>(base: T) {
  class TeardownMixin extends base {
    teardown$ = new Subject<void>();

    disconnectedCallback() {
      if (super.disconnectedCallback) { // これがHTMLElementにないよ！とわじわじいうのです。
        super.disconnectedCallback();
      }
      this.teardown$.next();
    }
  }

  return TeardownMixin;
}
```

HTMLElementの定義自体には`disconnectedCallback`はないので、TypeScript様が怒るのです。ただ、WebComponentのクラスなら、定義されている**可能性**があります。なので、その可能性をTypeScriptでこうして表現しておけば黙ってくれます。

```typescript
interface WebComponentWithPossibleConnectedCallback extends HTMLElement {
  disconnectedCallback?(): void;
}

export function TeardownableComponent<
  T extends Constructor<WebComponentWithPossibleConnectedCallback>,
>(base: T) {
  class TeardownMixin extends base {
    teardown$ = new Subject<void>();

    disconnectedCallback() {
      if (super.disconnectedCallback) { // コンパイラーが文句を言わんくなったど！
        super.disconnectedCallback();
      }
      this.teardown$.next();
    }
  }

  return TeardownMixin;
}
```

これでTypeScriptでもWeb ComponentのMixinができます。

# まとめ

これまでJavaScriptとTypeScriptで簡単なWeb ComponentのMixin記述法を紹介してきましたが、いかがでしょうか？

筆者はまだ直感的にMixinという概念に慣れていないので、若干違和感はありますが、PHPのようなTraitが使えるなら慣れるまでやるしかないと思っています。

まじゅん ちばらな！