ハイサイ!オースティンやいびーん
概要
JavaScriptでクラス継承の裏技をしてくれるMixinの記述法を紹介します。
Mixinとは
JavaScriptではES6以降、オブジェクト指向のクラス構文が可能になりました。これによって色々と関数のプロパティを継承させる方法が楽になったのですが、まだ欠点があります。
その欠点とは、一連のクラス継承じゃないと、いくつかの性質、プロパティ、そしてメソッドを含められないことです。
PHPでは、Traitを使うと、簡単に再利用可能なロジックを記述できます:
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
をしてメモリリークを防ぎたい時に、よく以下のような書き方をします。
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$
を持たせたいのです。
const TeardownableComponent = superClass =>
class extends superClass {
teardown$ = /** @type {import('rxjs').Subject<void>} */ (new Subject());
disconnectedCallback() {
if (typeof super.disconnectedCallback === "function") {
super.disconnectedCallback();
}
}
};
こうすると、上記のクラス定義を以下のように書き直せば、teardown$
を継承できるようになります。
class MyObserverComponent extends TeardownableComponent(HTMLElement) {
connectedCallback() {
fromEvent(this, "click")
.pipe(takeUntil(this.teardown$))
.subscribe(e => {
// Logic
});
}
}
JavaScriptだけなら、やはりシンプルです!
TypeScriptを使う場合
TypeScriptだと、ややこしくなります。まず、Constructor
の型を定義する必要があります。
引用:TypeScriptのドキュメントを参考にしています。
type Constructor<T = {}> = new (...args: any[]) => T;
次に、�この型を使ってMixinのロジックを書きますが、disconnectedCallback
辺りで問題になります。
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でこうして表現しておけば黙ってくれます。
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が使えるなら慣れるまでやるしかないと思っています。
まじゅん ちばらな!