エフアンダーバー

個人開発の記録

TypeScriptとVueのMixin

VueのMixinに関する(Webの世界では)一昔前の記事を読んでいて、

TypeScript 自体が Mixin 構文をサポートすればこのつらさもなくなるような気がしますが、 TypeScript 公式でこのめんどくさい Mixin のやり方を紹介しているので望みは薄いような気も……。

という記述を発見。

そういえばどこかのタイミングでTypeScriptがMixinに対応してたな、 と思ったのでどうにかできないかやってみました。

Mixinの問題

VueのコンポーネントをTypeScriptで書く方法のひとつのとして vue-class-componentを用いたデコレータによるものがあります。

@Component
class MyComponent extends Vue { ... }

しかし、この方法で単純にVueのMixinを利用しようとすると、

@Component({
    mixins: [ MyMixin ]
})
class MyComponent extends Vue { ... }

という記述になり、デコレータ内ですべて処理されてしまうため、Mixinされたメンバーが型として現れません。

そこで、Mixinされたメンバーの型情報を取り入れるため、Mixinしたクラスのインターフェースをimplementsします。

@Component({
    mixins: [ MyMixin ]
})
class MyComponent extends Vue implements MyMixin { ... }

一応実装としてはこれでいいのですが、 implementsするのでMixinクラスに既に書いた型情報を再度書き直すはめになります。

・・・で、TypeScriptにはMixin構文のサポートもあるし、これをもっと簡単に書けないか、というのが今回の話です。

解決方法

結論から言うとできました・・・おそらくMixin構文とは特に関係なく。

ジェネリクスを用いて、引数の型すべての交差型(intersection type)を返す関数を定義し、 その関数の戻り値を継承するようにすればいいようです。

import { Vue } from 'vue/types/vue';
import { ComponentOptions } from 'vue/types/options';

type VueCtor<V extends Vue> = { new(...args: any[]): V };
type VueClass<V extends Vue> = VueCtor<V> & typeof Vue;
type MixinType<V extends Vue> = ComponentOptions<V> | VueClass<V>;

function mixin<
    T extends VueClass<Vue>,
    M1 extends Vue>(
    Base: T,
    mixin1: MixinType<M1>): T & VueCtor<M1>;
// overloads ...
function mixin(Base: VueClass<Vue>, ...mixins: MixinType<Vue>[]) {
    return Base.extend({ mixins });
}
@Component
class Derived extends mixin(Base, FooMixin, BarMixin, ...) { ... }

一応Mixin構文も試してみたのですが、 コンストラクタとtypeof Vueの交差型を求めるComponentデコレータと、 純粋なコンストラクタを求めるMixin構文の相性が悪く、うまくいきませんでした。

ソースコード

TypeScript 'mixin' function for vue-class-componen ...

ライブラリ

記事にする前に軽くググってみたところ、 Awesome Vue TSというライブラリが同様の実装でMixin関数を公開しているようです。

軽く眺めた限りでは、vue-property-decoratorのようなデコレータやJSXへの対応など、 Vueをより型安全に使うための機能をまとめたライブラリのようです。

別の方法

そもそもなぜ元記事でMixin構文があれば楽なのに、という話になったかというと、 Mixinの型情報を書き直さなければならないためでした。

で、たまたま最近見かけたコードにこんなものが。

interface IFoo { ... }

interface Foo extends IFoo { }
class Foo implements IFoo { ... }

重要なのはinterface Foo extends IFoo {}の部分です。

TypeScriptでは同名のクラスとインターフェースの型情報はマージされます(Declaration Merging)。 なので、Fooインターフェースにメンバーを定義すると、Fooクラスのインスタンスもそのメンバーを持っていることに型の上ではなります。

ここでは、FooインターフェースはIFooインターフェースを継承しているため、 クラス定義内で明示的に書かなくてもFooクラスのインスタンスはIFooインターフェースのメンバーを型として持つことになります。

これと同様のことをMixinのケースですれば、次のように書けます。

interface MyComponent extends MyMixin { }
@Component({
    mixins: [ MyMixin ]
})
class MyComponent extends Vue { ... }

ほとんど試していないので、あらゆる場合に対応できるかどうかはわかりませんが、 特別な用意のいらないシンプルな解決策としては悪くないと思います。

未来の方法

さらにそもそもの話。 Mixinの処理はデコレータがやるんだから、その型付けもデコレータがやるべきだというのは多分皆の思うところだと思います。

現状、TypeScriptではデコレータは型情報に干渉できません(対象となる型を制限することはできますが)。 しかし、これではデコレータの能力が制限されてしまうということで提案が挙がっているようです。 もしこの提案が受け入れられれば、デコレータを付加するだけでMixinのメンバーにアクセスできるようになるかもしれません。

まあ、その時までVueが生きていればの話ですが。

おわりに

思いつきで始めて、試行錯誤の結果できたけど、探してみたらすでにあった、という徒労話なのですが、 なんか悔しかったのでいろいろ加えて記事にしてみました。 どうも自分は思いつきだけで行動しようとするところがあっていけない。 まあ、試した分だけ知識も付くので悪いことばかりではない(と思いたい)ですが。



執筆時のTypeScriptのバージョン:2.6.2