TypeScriptで、2つのオブジェクトをmix-inして、オブジェクトの機能を拡張する方法です。
やりたいこと
たとえば、下記のFooオブジェクトとBarオブジェクトを合成して、どちらのメソッドも持ったFooBarオブジェクトを作る方法を説明します。
class Foo {
getFoo(): string {
return 'foo'
}
}
class Bar {
getBar(): string {
return 'bar'
}
}
const foo = new Foo()
const bar = new Bar()
const fooBar = mixin(foo, bar)
fooBar.getFoo() // Fooのメソッドも
fooBar.getBar() // Barのメソッドも呼べる!
(伝わる人いるか分かりませんが、Scalaのnew Class with Traitみたいなイメージです)
実現方法
JavaScriptのProxyクラスを用います。簡単に言うと、FooオブジェクトをBarオブジェクトでラップするようなイメージです。
なので、Barオブジェクトにプロパティがあればそれが採用され、なければ、Fooオブジェクトのメソッドを採用する。どちらにも無ければundefinedといった処理になります。
次のmixin関数が、2つのオブジェクトを合成してくれる汎用的な関数の実装です:
const mixin = <T extends object, U extends object>(
object: T,
trait: U,
): Mixin<T, U> =>
new Proxy(object, {
get: (target, key) => (trait as any)[key] ?? (target as any)[key],
has: (target, key) => key in trait || key in target,
}) as any
type Mixin<T extends object, U extends object> = Omit<T, keyof U> & U
TypeScriptコードだと型がごちゃついてますが、JavaScriptの部分だけ取り出して見ると、何をしているかより分かりやすいと思います:
const mixin = (object, trait) => new Proxy(object, {
get: (target, key) => trait[key] ?? target[key],
has: (target, key) => key in trait || key in target,
})
mixin関数に2つのオブジェクトを渡せるようになっていて、2つ目のオブジェクトにプロパティがあればそれを採用、なければ1つ目のオブジェクトのものを採用、といったように先述した処理になっているのがわかるかと思います。
使い方
先程のfooとbarをmix-inしてみます:
const fooBar = mixin(foo, bar)
console.log(fooBar.getFoo()) //=> "foo
console.log(fooBar.getBar()) //=> "bar"
ミックスイン先のthisの参照
BarからFooのgetFoo()メソッドを使いたい場合、this.getFoo()で呼び出したいところですが、TypeScriptはBarがFooにミックスインされることは知らないので、「BarにはgetFooがないよ」というコンパイルエラーになってしまいます。
このようにミックスイン先のプロパティを参照する場合は、thisの型を明示する必要があります。
class Bar {
getBar(): string {
return 'bar'
}
getFoobar(this: Mixin<Foo, Bar>): string {
// ^^^^^^^^^^^^^^^^^^^^^
return this.getFoo() + this.getBar()
}
}