始めに
Vue.jsで昔以下の記事を書いたことがあり、$data
, $props
を書く習慣をつけていました。
しかしTypeScriptで型推論をするとこれらはanyと解釈されてしまうため、非常に使いづらくなってしまいました。
そんな時に、以下のようなVue.jsの実装をそのまま流用して型だけ当てるライブラリを見かけたので、同様なことができないかを試しました。
この記事ではTypedVue
と言うライブラリを自作して、これの紹介と実装方法についてまとめました。
ライブラリの機能
やっていることは非常にシンプルで、以下のことだけしています。
-
$data
,$props
に型をつける - this直下にdataやpropsの型を含めない
呼び出し方はVue.extend
と一緒の書き方で、完全推論版と、data, propsの型を伝えた書き方それぞれについて書きます。(比較のために通常の書き方とライブラリを使った書き方の両方を載せます)
完全型推論版
全ての型を推論させる方法で、それぞれ以下のように書きます。
import Vue from 'vue';
export default Vue.extend({
props: {
str: { type: String },
},
data() {
return {
value: 10,
localStr: this.str,
// $propsからのアクセスは可能だが、anyになってしまう
// localStr: this.$props.str,
};
},
computed: {
_double(): number {
return 2 * this.value;
// $dataからのアクセスは可能だが、anyになってしまう
// return 2 * this.$data.value;
}
},
});
import TypedVue from '@wintyo/typed-vue';
export default TypedVue.typedExtend({
props: {
str: { type: String },
},
data() {
return {
value: 10,
localStr: this.$props.str,
// this直下をアクセスするとエラーになる
// localStr: this.str,
};
},
computed: {
_double(): number {
return 2 * this.$data.value;
// this直下をアクセスするとエラーになる
// return 2 * this.value;
},
},
});
props, data型の定義版
上記の方法でもある程度型はつけられますが、特にpropsは推論が難しく、Date
を渡すとなぜかstring
と推論されてしまったりする問題があります。
そこでprops, dataは先に定義し、その定義に合わせて設定コードを書くようにする方法は以下のようになります。
慣れないとめんどくさいかもしれませんが、Reactとかみると先に型を定義してますし、そもそも推論でどうにかしようとする方が無理があるように感じました。
import Vue from 'vue';
import { RecordPropsDefinition } from 'vue/types/options';
interface IProps {
str: string;
date: Date;
}
interface IData {
value: number;
localStr: string;
}
export default Vue.extend({
props: {
str: { type: String },
date: { type: Date },
} as RecordPropsDefinition<IProps>,
data(): IData {
return {
value: 10,
localStr: this.str,
};
},
created() {
this.date; // ちゃんとDate型と認識される
},
});
import TypedVue, { RecordPropsDefinition } from '@wintyo/typed-vue';
// 以下と同じ
// import { RecordPropsDefinition } from 'vue/types/options';
interface IProps {
str: string;
date: Date;
}
interface IData {
value: number;
localStr: string;
}
export default TypedVue.typedExtend({
props: {
str: { type: String },
date: { type: Date },
} as RecordPropsDefinition<IProps>,
data(): IData {
return {
value: 10,
localStr: this.$props.str,
};
},
created() {
this.$props.date; // ちゃんとDate型と認識される
}
});
実装方法
ここからはライブラリの実装の中身になります。
簡単に言えばanyになってしまうところに上手く型が当たるように追加する感じになります。
ただ事前に型が乗っているせいで、設定は非常にやりづらいです。
$data, $propsのany定義を止める
まずVue.jsはデフォルトでRecord<string, any>
になっているせいで、そこに型がついてもこちらが優先されて型エラーになることができません。
そこで空オブジェクトにオーバーライドさせて、それを使うようにします。少し厄介なのが、Vueは実はVueConstructor<V>
の型であるため、それに合わせて上手くキャストさせます。
export interface Vue {
readonly $data: Record<string, any>;
readonly $props: Record<string, any>;
}
import Vue from 'vue';
import { Vue as IVue, VueConstructor as IVueConstructor } from 'vue/types/vue';
export interface ITypedVue extends IVue {
readonly $props: {};
readonly $data: {};
}
export const TypedVue = Vue as IVueConstructor<ITypedVue>;
data, propsの型を$data, $propsに当てる
詳細は以下の記事に譲りますが、CombinedVueInstance
のData
とProps
の部分が{ $data: Data }
, { $props: Props }
のように入れば型がつきます。ついでにthis直下に型がつくのもなくせますね。
// デフォルト
ThisType<CombinedVueInstance<V, Data, Methods, Computed, Props>>
// 拡張
ThisType<CombinedVueInstance<V, { $data: Data }, Methods, Computed, { $props: Readonly<Props> }>>
このような変更を一つずつ行いますが、既存の型を変更するわけにはいかないので、新しく型を作って上記のような微妙な変更を行っています。
新しく定義した名前で動作するようにする
typedExtend
と言う名前にしたが、このメソッドでも動くようにメソッドを拡張しました。
const typedExtend = function (this: any, options: any) {
const instance = this.extend(options);
instance.typedExtend = typedExtend;
return instance;
}
Vue.typedExtend = typedExtend;
終わりに
通常では$data
, $props
はanyになっているので、そこに型を付けるライブラリの紹介と実装方法について書きました。割と強引な実装方法なので少しでも型定義が変わると動かなくなりますが、Vue 3.0になりますしOptions APIはこれ以上変わらないだろうなと思っています。
Vue 3.0はComposition APIになりますが、書き方がカオスにならないかとか、大幅な変更でライブラリ周りの連携とか心配ですし、もう少し様子見なのかなと思いました。Class形式だったらすぐにでも乗り移るんですけどね。。。