Vue.js options APIの型推論について調べてみた

# 始めに
Vue.jsは今だと`Vue.extend`を呼ぶことでobject形式でも型が利くようになり、クラス形式じゃなくてもTypeScriptは機能するようになってきました。
ただ具体的な原理が分からなかったので調べてみることにしました。バージョンは2.6.11をみています。

```typescript
import Vue from 'vue';

export default Vue.extend({
  data() {
    return {
      value: 10,
    };
  },
  mounted() {
    this.value.slice();  // number型なのでエラー！
  },
});
```

# 型推論の基本原則
推論の流れをみていく前に、基本的なところをおさらいします。

## generics
型推論はgenericsを使ってパラメータから何の型かを推論します。
`T`が何らかの型で、それは受け取ったパラメータに応じて変化します。

```typescript
function head<T>(arr: Array<T>): T | undefined {
  return arr[0];
}

// number型と解釈される
const num = head([1, 2, 3]);
// string型と解釈される
const str = head(['one', 'two', 'three']);

// 型が分からない場合は明示的に書くこともできる
const list: any = [1, 2, 3];
const item = head<number>(list);
```

## オーバーロード
同じメソッド名を定義して、引数に応じて対応したメソッドの定義を参照します。

```typescript
class Twicer {
  // オーバーロードメソッドのインターフェイス
  twice(num: number): number // 数を2倍する処理のインターフェイス
  twice(str: string): string // 文字列を2回繰り返す処理のインターフェイス
  twice<T>(arr: T[]): T[]    // 配列要素を2倍にする処理のインターフェイス

  twice(value: any): any {
    // 具体的な実装
  }
}

const twicer = new Twicer();
twicer.twice(10);        // twice(num: number): numberをみる
twicer.twice('abc');     // twice(str: string): stringをみる
twicer.twice([1, 2, 3]); // twice<T>(arr: T[]): T[]をみる（ここではT=number）
```

実装は共通のメソッド一つでやらないといけないとか、TypeScriptだと色々制約があるのですが、今回の話とは関係ないので詳細は以下などを参照してください。
[TypeScript： オーバーロードメソッドを定義する方法 - Qiita]( https://qiita.com/suin/items/7d6837a0342b36891099)

# Vue.jsの推論
ここから推論の流れについてみていきたいと思います。
`Vue.extend`の型定義はこちらになります。定義が長い上オーバーロードが5つもあってなんか発狂したくなりますね・・・。

```typescript:extendの定義
export interface VueConstructor<V extends Vue = Vue> {
  // propsが名前指定の場合
  extend<Data, Methods, Computed, PropNames extends string = never>(options?: ThisTypedComponentOptionsWithArrayProps<V, Data, Methods, Computed, PropNames>): ExtendedVue<V, Data, Methods, Computed, Record<PropNames, any>>;
  // propsをオブジェクトで指定した場合（これを調べる）
  extend<Data, Methods, Computed, Props>(options?: ThisTypedComponentOptionsWithRecordProps<V, Data, Methods, Computed, Props>): ExtendedVue<V, Data, Methods, Computed, Props>;
  // 関数型のpropsが名前指定の場合
  extend<PropNames extends string = never>(definition: FunctionalComponentOptions<Record<PropNames, any>, PropNames[]>): ExtendedVue<V, {}, {}, {}, Record<PropNames, any>>;
  // 関数型のpropsをオブジェクトで指定した場合
  extend<Props>(definition: FunctionalComponentOptions<Props, RecordPropsDefinition<Props>>): ExtendedVue<V, {}, {}, {}, Props>;
  // それ以外
  extend(options?: ComponentOptions<V>): ExtendedVue<V, {}, {}, {}, {}>;
}
```

https://github.com/vuejs/vue/blob/v2.6.11/types/vue.d.ts#L86-L90

とりあえず、今回知りたいのは以下のようなoptionを渡したことにしたいので、2番目のextendの型について調べたいと思います。

```typescript
import Vue from 'vue';

export default Vue.extend({
  props: {
    hoge: String,
  },
  data() {
    return {
      value: 10,
    };
  },
  computed: {
    _double() {
      return 2 * this.value;
    },
  },
  methods: {
    action() {
      console.log('Action!!');
    },
  },
});
```

今回見たいものだけを抜粋して整理すると、以下のようになります。

```typescript:extendの定義
export interface VueConstructor<V extends Vue = Vue> {
  // propsをオブジェクトで指定した場合
  extend<Data, Methods, Computed, Props>(
    options?: ThisTypedComponentOptionsWithRecordProps<V, Data, Methods, Computed, Props>
  ): ExtendedVue<V, Data, Methods, Computed, Props>;
}
```

+ Data, Methods, Computed, Propsを推論
  + 推論するために`ThisTypedComponentOptionsWithRecordProps`でラップ
+ `ExtendedVue`を返す

推論する道のりがあまりにも長いので、上記のパラメータを渡した際に推論された結果を頭に入れた上で話を進めていきたいと思います。

```typescript:推論された結果
Data = { value: number };
Methods = { action(): void };
Computed = { _double: number };
Props = { hoge: string };
```

## 返り値の型

まず返ってくる型を見ます。元々のVueの情報に推論された結果を結合して同じ`VueConstructor`を返します。これでextendを継続して使えますね。

```typescript:ExtendedVue
export type ExtendedVue<Instance extends Vue, Data, Methods, Computed, Props> =
  VueConstructor<CombinedVueInstance<Instance, Data, Methods, Computed, Props> & Vue>;
```

`CombinedVueInstance`は簡単で、単純にDataやMethodsなどを結合しているだけです。

```typescript:CombinedVueInstance
export type CombinedVueInstance<Instance extends Vue, Data, Methods, Computed, Props> =
  Data & Methods & Computed & Props & Instance;
```

## 引数側の型
続いて引数側の`ThisTypedComponentOptionsWithRecordProps`について見ていきます。
更に`ComponentOptions`でラップされて泣きそうになりますが、先に3つ目の`ThisType`について説明します。これはthisの型を定義しており、`this.value`みたいなアクセスができるようになります。`CombinedVueInstance`は返り値の型の説明でもした全部の型を結合するやつですね。

```typescript:ThisTypedComponentOptionsWithRecordPropsの型定義
export type ThisTypedComponentOptionsWithRecordProps<V extends Vue, Data, Methods, Computed, Props> =
  object &
  ComponentOptions<V, DataDef<Data, Props, V>, Methods, Computed, RecordPropsDefinition<Props>, Props> &
  // this.~で書ける型の定義
  ThisType<CombinedVueInstance<V, Data, Methods, Computed, Readonly<Props>>>;
```

さらに入って`ComponentOptions`の定義をみます。ここでようやくdataとか、propsとか見慣れたプロパティを目にします。ここで書かれた内容が推論されることになります。
genericsでデフォルトの型が代入されていますが、親からは全部渡されているので今回は無視して大丈夫です。
次から各プロパティについて簡単な順で見ていきます。

```typescript:ComponentOptions
export interface ComponentOptions<
  V extends Vue,
  Data = DefaultData<V>,
  Methods = DefaultMethods<V>,
  Computed = DefaultComputed,
  PropsDef = PropsDefinition<DefaultProps>,
  Props = DefaultProps
> {
  data?: Data;
  props?: PropsDef;
  propsData?: object;
  computed?: Accessors<Computed>;
  methods?: Methods;

  // 他は省略
}
```

### methodsの推論
methodsが一番簡単で、methodsプロパティに書かれた内容がそのまま`Methods`の型として推論されます。


```typescript
// こういうプロパティだと
{
  methods: {
    action() {
      console.log('Action!!');
    },
  },
}

// こういう風に推論される
Methods = { action(): void };
```

余談ですが何もチェックしていないのでここに値をセットしてもエラーにならないですね(笑)。genericsの`Methods`に何かしらextendsしていたらメソッドだけとか縛れそうですけどね。

```typescript
{
  methods: {
    hoge: 'not error!',  // エラーにならない
    action() {
      console.log('Action!!');
    },
  },
}
```

### dataの推論
`Data`って書かれてますが、実際親から呼ばれているのは`DataDef`になっているので注意してください。
その`DataDef`は以下のような定義になっています。

```typescript:DataDef
type DataDef<Data, Props, V> = Data | ((this: Readonly<Props> & V) => Data)
```

そのまま書いた場合はそれを`Data`の型にして、メソッドの場合はthisに`Props`とVueの型を持って`Data`を返すという定義になっています。

そういえばdataプロパティのメソッドではpropsの値も使えましたね。その辺の定義もキチンとしているようです。

### computedの推論
computedは`Accessors<Computed>`で推論されます。
Accessorsの定義は以下です。

```typescript:Accessors
export type Accessors<T> = {
  [K in keyof T]: (() => T[K]) | ComputedOptions<T[K]>
}

export interface ComputedOptions<T> {
  get?(): T;
  set?(value: T): void;
  cache?: boolean;
}
```

各プロパティにおいてメソッドで書くかget, setのようなオブジェクトを更に含めるかの判定をしているようです。よくこれで推論できますね・・・。
余談ですが、推論するのが難しいため返り値には型を書くように推奨されていますね。
[TypeScript のサポート — Vue.js](https://jp.vuejs.org/v2/guide/typescript.html#%E6%88%BB%E3%82%8A%E5%80%A4%E3%81%AE%E5%9E%8B%E3%81%AB%E3%82%A2%E3%83%8E%E3%83%86%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E3%81%A4%E3%81%91%E3%82%8B)

### propsの推論
最後にpropsですが、推論の道は長いです（汗）。
まずgenericsの型には`PropsDef`と`Props`があるのですが、`PropsDef`の方を見れば良さそうです。（よく分かっていませんが、`Props`の方はrenderのところで使われていました）
`PropsDef`の方は親からは`RecordPropsDefinition<Props>`で定義されているので、これの定義をみると以下のようになります。関連する型も載せましたが多いですね・・・。

```typescript:RecordPropsDefinition
export type RecordPropsDefinition<T> = {
  // なぜかqiita上で表示されなかったので先頭に#をつけています。実際はないので注意してください。
# [K in keyof T]: PropValidator<T[K]>;
}

export type PropValidator<T> = PropOptions<T> | PropType<T>;

export interface PropOptions<T=any> {
  type?: PropType<T>;
  required?: boolean;
  default?: T | null | undefined | (() => T | null | undefined);
  validator?(value: T): boolean;
}

export type PropType<T> = Prop<T> | Prop<T>[];

export type Prop<T> =
  { (): T } |
  { new(...args: never[]): T & object } |
  { new(...args: string[]): Function }
```

詳細は省きますが、最終的には`Prop<T>`のところまで行き、コンストラクタの型をみることになります。

ちなみに`Prop<T>`の型定義があんまりよくなくって、1つ目の`{ (): T }`がコンストラクタじゃないせいでJSのDate型はstring型と解釈されてしまいます・・・。

#### 余談
propsDataというプロパティがありましたが、どうやらテストの時に便利なプロパティらしいです。

https://qiita.com/ykhirao/items/43397531e58664ef4645

## 補足資料
今回の型推論を理解するために手書きのノートを書いているのでそれも載せておきます。

![3b46028f-5efe-4355-bd34-a759db9c4405.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/187768/5baf25b1-b593-98d5-4a3d-b876e17a8e77.jpeg)
![120afc6f-fefa-447d-ac77-e985652d3db1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/187768/c417f379-184e-2957-3aaa-011147bc61c1.jpeg)
![beb1e0a5-0789-445f-92a8-65d08afe0de6.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/187768/3cbdd4d4-13d7-af69-814b-7d8dacab228b.jpeg)

# 型推論調査の目的
以上がoptionsを渡した時の推論の流れでした。なかなか難しいですね・・・。
今更ながらこれを調べた理由を説明しますと、以下の2つありました。

## プラグインを作った際にoptionsで定義したものを使いたい
僕は定数をtemplate側でも使えるようなプラグインを作ったのですが、Vue.jsのプロパティを今はとりあえずanyにしちゃっていて、できればoptionの設定から見れたないいなぁって思っていました。

[@wintyo/vue-constants-injection-plugin - npm](https://www.npmjs.com/package/@wintyo/vue-constants-injection-plugin)

```typescript:vue-constants-injection-pluginの型定義
// extend option
declare module 'vue/types/options' {
  interface ComponentOptions<V extends Vue> {
    C?: {
      [key: string]: any;
    }
  }
}

// extend property
declare module 'vue/types/vue' {
  interface Vue {
    $C: {
      // できればComponentOptionsから定義したものを使いたい！
      [key: string]: any;
    };
  }
}
```

まぁ今回調べた結果だと**ほぼ無理**ですね(笑)。extendsを更にオーバーロードさせたらもしかしたらできるかもしれませんが、あれ以上カオスにはしたくないですね・・・。


## $data, $propsに型をつけたい
昔以下の記事を投稿しましたが、`$data`, `$props`を使っていきたいという気持ちをずっと持ち続けています。

[Vueのプロパティを分かりやすくする - Qiita](https://qiita.com/wintyo/items/e6918779b837b209a1e3)

しかしVue.jsの型定義をみると`Record<string, any>`になって全く型がつかないのが現状です。

https://github.com/vuejs/vue/blob/v2.6.11/types/vue.d.ts#L33-L34

今回調べた感じだと`Props`とかを普通に`$props`にも適応してあげれば解決しそうですが、元々がanyなので結局エラー扱いにはできないというもどかしさがあります・・・。
これはいつかまた別な記事で書くかもしれません。

2020/04/07追記
こちらにプラグインを自作した話を書きました！
[$data, $propsに型をつけるプラグインを作る](https://qiita.com/wintyo/items/3ed1abadf22bf452fcf6)

# 終わりに
Vue.jsのoptionsを渡した時の推論の流れについて調べましたが、かなり大変でした・・・。
Vue.js 3.0からはComposition APIがリリースされる予定ですが、こうした背景からoptions APIは使われなくなるのかなぁって思ってきました。ただComposition APIだと`$router`とかその辺の注入が一切なくなってしまうので移行は結構大変そうだなぁって思いました。Classベースがネイティブでサポートされたらいいんですけどね・・・。