10
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

TypeScript: Proxyを使ってオブジェクトのmix-inを実現する方法

Last updated at Posted at 2020-04-20

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つ目のオブジェクトのものを採用、といったように先述した処理になっているのがわかるかと思います。

使い方

先程のfoobarをmix-inしてみます:

const fooBar = mixin(foo, bar)

console.log(fooBar.getFoo()) //=> "foo
console.log(fooBar.getBar()) //=> "bar"

ミックスイン先のthisの参照

BarからFoogetFoo()メソッドを使いたい場合、this.getFoo()で呼び出したいところですが、TypeScriptはBarFooにミックスインされることは知らないので、「BarにはgetFooがないよ」というコンパイルエラーになってしまいます。

TypeScript__Playground_-_An_online_editor_for_exploring_TypeScript_and_JavaScript.png

このようにミックスイン先のプロパティを参照する場合は、thisの型を明示する必要があります。

class Bar {
  getBar(): string {
    return 'bar'
  }
  
  getFoobar(this: Mixin<Foo, Bar>): string {
//          ^^^^^^^^^^^^^^^^^^^^^
    return this.getFoo() + this.getBar()
  }
}
10
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?