$data, $propsに型をつけるプラグインを作る

# 始めに
Vue.jsで昔以下の記事を書いたことがあり、`$data`, `$props`を書く習慣をつけていました。
しかしTypeScriptで型推論をするとこれらはanyと解釈されてしまうため、非常に使いづらくなってしまいました。

+ [Vueのプロパティを分かりやすくする](https://qiita.com/wintyo/items/e6918779b837b209a1e3)
+ [Vue.js options APIの型推論について調べてみた](https://qiita.com/wintyo/items/1d51a49e86332a12ee93)

そんな時に、以下のようなVue.jsの実装をそのまま流用して型だけ当てるライブラリを見かけたので、同様なことができないかを試しました。

+ [TypeScriptでVue.set/deleteを型安全にするライブラリ作った](https://qiita.com/simochee/items/89f4b17fe971b4571961)

この記事では`TypedVue`と言うライブラリを自作して、これの紹介と実装方法についてまとめました。

https://www.npmjs.com/package/@wintyo/typed-vue

# ライブラリの機能
やっていることは非常にシンプルで、以下のことだけしています。

+ `$data`, `$props`に型をつける
+ this直下にdataやpropsの型を含めない

呼び出し方は`Vue.extend`と一緒の書き方で、完全推論版と、data, propsの型を伝えた書き方それぞれについて書きます。（比較のために通常の書き方とライブラリを使った書き方の両方を載せます）

## 完全型推論版
全ての型を推論させる方法で、それぞれ以下のように書きます。

```typescript:通常
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;
    }
  },
});
```

```typescript:TypedVue
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とかみると先に型を定義してますし、そもそも推論でどうにかしようとする方が無理があるように感じました。

```typescript:props,data型の定義版（通常）
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型と認識される
  },
});
```

```typescript:props,data型の定義版（TypedVue）
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>`の型であるため、それに合わせて上手くキャストさせます。

```typescript:デフォルト
export interface Vue {
  readonly $data: Record<string, any>;
  readonly $props: Record<string, any>;
}
```

```typescript:拡張
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直下に型がつくのもなくせますね。

+ [Vue.js options APIの型推論について調べてみた](https://qiita.com/wintyo/items/1d51a49e86332a12ee93)


```typescript:ThisTypeの拡張
// デフォルト
ThisType<CombinedVueInstance<V, Data, Methods, Computed, Props>>

// 拡張
ThisType<CombinedVueInstance<V, { $data: Data }, Methods, Computed, { $props: Readonly<Props> }>>
```

このような変更を一つずつ行いますが、既存の型を変更するわけにはいかないので、新しく型を作って上記のような微妙な変更を行っています。

https://github.com/wintyo/typed-vue/blob/v0.0.2/lib/index.ts#L12-L15

## 新しく定義した名前で動作するようにする
`typedExtend`と言う名前にしたが、このメソッドでも動くようにメソッドを拡張しました。

```typescript:メソッドの拡張
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形式だったらすぐにでも乗り移るんですけどね。。。