Vuexの型定義を覗いてみる〜その１〜

TypeScriptでVuexを用いた開発を行っていくと、ジェネリクスを駆使してstateなどの型情報を付与していくことになります。サンプルコードなどを参考に実装していくと、こうすればいいんだろうなという表面上の解は得られるのですが、どうしてそうしなければならないのかイマイチよくわからないまま実装していくことがしばしばあります。stateなどの補完がされることは確認できるのですが、どうしてできるのかちょっと疑問に思いました。

よくわからないまま実装するのも良くないので、今回はVuexでStoreを実装していくときにTypeScriptでどう型情報が付与されていくのかを見ていきたいと思います。

今回の調査のためのサンプルコードはこちらになります。

https://github.com/nasum/TypedTodoList

## すべてのはじまり `new Vuex.Store<RootState>()`

まずStoreを作成するため `new Vuex.Store<RootState>()` を実行します。

```typescript
// 細かい部分は省略しています
import Form, { FormState } from "./Form";
import List, { ListState } from "./List";

export interface MyRootState {
  Form: FormState;
  List: ListState;
}

const store = new Vuex.Store<MyRootState>({
  modules: {
    Form,
    List
  }
});
```

`Form` と `List` のStoreをimportしてモジュールにセットしています。`FormState` と `ListState` はStateの型情報で、次のように定義されています。

```typescript
export type FormState = {
  text: string;
};
```

```typescript

type Todo = {
  text: string;
};

export type ListState = {
  todoList: Todo[];
};
```

`FormState` と `ListState` をもつ `MyRootState` を定義し、 `new Vuex.Store<MyRootState>()` のようにセットすることでstateの補完が効くようになります。

ここで型情報をセットすることでVSCodeなどで補完が効くことが確認できます。

![Screenshot from 2019-12-07 15-12-59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/11510/3d699310-4333-c80d-cda9-aab34b59005d.png)

`MyRootState` をセットしないと補完が効かないことも確認できます。

![Screenshot from 2019-12-07 15-13-19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/11510/484f0c65-04f7-601c-bf4b-c3c606ddc2e6.png)


（どうでもいい話ですがジェネリクスで型情報をセットするときの正しい言葉の使い方がわかりません。ジェネリクスとして○○をセットする？）


## セットされたMyRootStateがどのように働くのか

`MyRootState` をセットすることで型情報が付与され補完が効くことが確認できました。次にどうして補完が効くようになるのか見ていきたいと思います。

まず `Vuex.Store` の型がどのように実装されているのかを見てみます。

https://github.com/vuejs/vuex/blob/d3979fddd589df7d8d74f9bf0772752675f50abb/types/index.d.ts#L10-L14

```typescript:vuex/types/index.ts
export declare class Store<S> {
  constructor(options: StoreOptions<S>);

  readonly state: S;
  readonly getters: any;

  // 以下省略
}
```

`Store` で入力される型情報 `S` 、つまり今回わたしている `MyRootState` はコンストラクタで使用されている `StoreOption` や `readonly State` などで使われていることがわかります。

`readonly state: S;` でStoreから参照できるstateに型情報が付与され、補完が効くようになっているようです。

ちなみに `readonly` とは何なのでしょうか？

TypeScriptの `readonly` とは任意のプロパティを読み取り専用にするためにマークできる機能で、マークしたプロパティを上書きしようとするとコンパイルエラーにしてくれます。

![Screenshot from 2019-12-07 16-03-31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/11510/f935c16e-6a97-7549-b6d6-a21ab309bee1.png)

簡単に見ていきましたが、storeから `Form` や `List` のStateが補完されるのは `Store` の型定義で `state` のプロパティに入力された `MyRootState` の型情報をわたしているからのようです。意外とあっさりと答えにたどり着きました。

## Modulesに不正なModuleが登録されない仕組み

流石にstateの型補完だけだと内容があまりにも足りないのでmodulesでわたしているmoduleで不正なmoduleが渡らないように型で守る仕組みについて見ていきます。

ますmoduleはStoreの引数のオブジェクトにして渡されます。Storeの引数として渡されるオブジェクトは `StoreOptions` という型に沿ったオブジェクトとして渡す必要があります。`StoreOptions` の型情報は次のようになります。

https://github.com/vuejs/vuex/blob/d3979fddd589df7d8d74f9bf0772752675f50abb/types/index.d.ts#L90-L98

```typescript:vuex/types/index.ts
export interface StoreOptions<S> {
  state?: S | (() => S);
  getters?: GetterTree<S, S>;
  actions?: ActionTree<S, S>;
  mutations?: MutationTree<S>;
  modules?: ModuleTree<S>;
  plugins?: Plugin<S>[];
  strict?: boolean;
}
```

それぞれのプロパティに`?`がついています。`?` のついてプロパティは省略可能なプロパティです。今回のサンプルコードでは `modules` しか入れていませんでした。その他は `?` がついているので省略可能だったわけです。

逆にここで列挙されていないプロパティを追加しようとしてもコンパイルエラーになります。

![Screenshot from 2019-12-07 17-14-50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/11510/30cb0e73-116b-7299-2300-84dc0b826c0e.png)

hogeというプロパティを追加しようとしてもエラーになることがわかります。


`ModuleTree` の型定義は次のようになります。

https://github.com/vuejs/vuex/blob/d3979fddd589df7d8d74f9bf0772752675f50abb/types/index.d.ts#L136-L138


```typescript:vuex/types/index.ts
export interface ModuleTree<R> {
  [key: string]: Module<any, R>;
}
```

`ModuleTree` の中身はキーとそれに対応する `Module` の型定義となっています。

なので `Module` を実装していない値を登録しようとするとコンパイルエラーになります。

![Screenshot from 2019-12-07 17-17-32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/11510/4917ae9f-06b9-17d1-11a9-754c859d1056.png)

`test`というプロパティに number型の1をセットするとコンパイルエラーになります。

次に`Module` の型定義を見ていきます。

https://github.com/vuejs/vuex/blob/d3979fddd589df7d8d74f9bf0772752675f50abb/types/index.d.ts#L111-L118

```typescript:vuex/types/index.ts
export interface Module<S, R> {
  namespaced?: boolean;
  state?: S | (() => S);
  getters?: GetterTree<S, R>;
  actions?: ActionTree<S, R>;
  mutations?: MutationTree<S>;
  modules?: ModuleTree<R>;
}
```

`Module` は

* namespaced
* state
* getters
* actions
* mutations
* modules

のプロパティを持ちます。 それぞれのプロパティによって入る型の定義が違うので、例えばactionsプロパティに期待しない型が入るとコンパイルエラーになります。

```typescript

const Sample = {
  actions: []
}

const store = new Vuex.Store<MyRootState>({
  plugins: [createLogger({})],
  modules: {
    Form,
    List,
    Sample //actionsはActionTreeを実装していないのでコンパイルエラーになる。
  },
});
```

VuexはTypeScriptで作られているわけではないので、素のJavaScriptのままだと不正なモジュールが登録されてもコンパイル時にわからずランタイムエラーになるのですが、型定義ファイルが定義されTypeScriptで書けるようになると、不正な値はコンパイルエラーで事前にわかるのでだいぶ開発しやすくなります。

`dispatch` や `commit` 時にはまだ課題があるのですがTypeScriptで書いて少なくともマイナスになることはないと思います。

## まとめ

以上。Vuexの型定義について見ていきました。記事を書くためにVuexの型定義ファイルを眺めていたのですが、型をあとづけするのはかなり大変そうでした。 `ActionTree` や `ActionHandler` あたりはかなりの苦労が伺えます。

型定義を眺めているとTypeScriptの勉強になるので、Vuex周りの型定義については引き続き眺めていこうと思います。