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()
}
}