NuxtJS+TypeScriptの開発記録 (2) vuex-class-componentの導入

NuxtJS+[Vuex](https://vuex.vuejs.org/)とTypeScriptを組み合わせるときの**不満**は主に：

* `this.$store`は型情報がない＝**補完もチェックも効かない**
* ミューテーションを`'increment'`みたいに**文字列で**`commit`する＝**補完もチェックも効かない**

これじゃあせっかくTypeScriptにする意味が半減してしまう。そこで、[公式ページに紹介](https://typescript.nuxtjs.org/ja/cookbook/store.html)されていた[vuex-class-component](https://github.com/michaelolof/vuex-class-component)を使ってみる。[^1]

[^1]: [vuex-module-decorators](https://github.com/championswimmer/vuex-module-decorators)の方が有名なようなのだが、Usageに`@Action({ commit: 'increment' })`と文字列で指定してるところがあったので、希望に沿わなかった。

## サンプル（ライブラリ導入前）

よくあるシンプルなカウンターのサンプル。

```ts:store/counter.ts
import { MutationTree } from 'vuex';

// 本当はクラスで書きたい……
export const state = () => ({
  count: 0
});

export type RootState = ReturnType<typeof state>;

export const mutations: MutationTree<RootState> = {
  increment: state => state.count++
};
```

```ts:store/index.ts
export const state = () => ({});
```

```vue:components/counter.vue
<template>
  <div>
    <p>count: {{ count }}</p>
    <button @click="increment()">
      +
    </button>
  </div>
</template>
<style></style>
<script lang="ts">
import { Vue, Component } from 'vue-property-decorator';

@Component
export default class Counter extends Vue {
  public get count(): number {
    return this.$store.state.counter.count; // $storeはany 補完やチェックが効かない
  }

  public increment(): void {
    this.$store.commit('counter/increment'); // 文字列なので補完やチェックが効かない
  }
}
</script>
```

## ライブラリ導入

```
$ yarn add -D vuex-class-component
```

[ストア | Nuxt TypeScript](https://typescript.nuxtjs.org/ja/cookbook/store.html#vuex-class-component)によると、バージョン2.2.1現在、New APIの実装はまだNuxtJSとの互換性が完璧ではなく、今回はOld APIを使う必要がありそうだ。

```ts:store/counter.ts
import { Module, mutation, VuexModule } from 'vuex-class-component';

// クラスで書けた！
@Module({ namespacedPath: 'counter', target: 'nuxt' })
export class CounterStore extends VuexModule {
  public count = 0;

  @mutation increment(): void {
    this.count++;
  }
}
```

```ts:store/index.ts
import Vuex from 'vuex';
import { CounterStore } from './counter';

export const store = new Vuex.Store({
  modules: {
    counter: CounterStore.ExtractVuexModule(CounterStore)
  },
  strict: false // 下記のVXMを使うのに必要
});

export interface VXM {
  counter: CounterStore;
}

// ストアに型情報つきでアクセスできるプロキシオブジェクト
export const vxm: VXM = {
  counter: CounterStore.CreateProxy(store, CounterStore)
};
```

上記の`vxm`を使うと、以下のように型情報つきでストアにアクセスできる。

```vue:counter.vue（script部分のみ抜粋）
<script lang="ts">
import { Vue, Component } from 'vue-property-decorator';
import { vxm } from '@/store';

@Component
export default class Counter extends Vue {
  public get count(): number {
    return vxm.counter.count; // 補完とチェックが効く！
  }

  public increment(): void {
    vxm.counter.increment(); // メソッドとして呼び出せる！
  }
}
</script>
```

ストアに新しいクラスを追加するたびに`store/index.ts`の`store`と`VXM`と`vxm`を修正する必要はあるが、まぁこれは許容範囲とする。

## VXMをNuxtJSのプラグイン化する

コンポーネントで毎回`vmx`をインポートするのは面倒なので、[NuxtJSのプラグイン機能](https://ja.nuxtjs.org/guide/plugins)を使って、`this.$store`と同じように、`this.$vxm`でアクセスできるようにしてみる。

```ts:plugins/vmx.ts
import Vue from 'vue';
import { vxm } from '@/store';

Vue.prototype.$vxm = vxm;
```

```diff:nuxt.config.ts（抜粋）
-  plugins: [],
+  plugins: ['~/plugins/vxm.ts'],
```

これでプラグインの追加は完了なのだが、`Vue`インターフェースには`$vxm`の情報がないため、TypeScript的には使えない。
そこで、前回追加した`vue-shim.d.ts`ファイルに`vxm`の型定義を追加し、以下のようにする。

```ts:vue-shim.d.ts
import Vue from 'vue';
import { VXM } from './store';

declare module '*.vue' {
  export default Vue;
}

// 既存の型定義に追加する記法
declare module 'vue/types/vue' {
  interface Vue {
    $vxm: VXM;
  }
}
```

コンポーネント側は以下のように修正。

```diff:counter.vue（script部分のみ抜粋）
 <script lang="ts">
 import { Vue, Component } from 'vue-property-decorator';
-import { vxm } from '@/store';

 @Component
 export default class Counter extends Vue {
   public get count(): number {
-    return vxm.counter.count; // 補完とチェックが効く！ 
+    return this.$vxm.counter.count; // 補完とチェックが効く！ 
   }

   public increment(): void {
-    vxm.counter.increment(); // メソッドとして呼び出せる！ 
+    this.$vxm.counter.increment(); // メソッドとして呼び出せる！ 
   }
 }
 </script>
```

これで、`this.$store`と同じような感覚で使える型情報つきの`this.$vxm`が手に入った。
とりあえずこの構成で開発してみる。

## 参考にしたページ

* [ストア | Nuxt TypeScript](https://typescript.nuxtjs.org/ja/cookbook/store.html)
* [nuxt+typescript+vuex-class-component+vuexfireに成功しました](https://qiita.com/hikaruna/items/fff8ed49556e659b9a06)
* [vuex-class-componentを使ってVuexをクラススタイルでタイプセーフに書いてみよう](https://dev.classmethod.jp/client-side/spa/typesafe-vuex-by-class-style/)