nuxt + typescript + vuex + axios に手を焼いたので共有


こんにちは
nuxt + typescript でフロントエンドを作っているエンジニアです。
store の getter や actions にも type を効かせるときに手を焼いたので共有します。

## 目次

* [ベースの作成](#ベースの作成)
* [公式の見解](#公式の見解)
* [vuex-module-decorators で型付け](#vuex-module-decorators-で型付け)
* [vuex-class-component で型付け](#vuex-class-component-で型付け)
* [vuex-class-component での注意点](#vuex-class-component-での注意点)
* [まとめ](#まとめ)
* [axios の設定](#axios-の設定)

## ベース作成

nuxt × typescript の構築は他の記事に譲るとし、今回の説明で必要なファイルを列挙します。
なお、今回は shops を中心に store の説明をします。

まずは型定義

```types/index.d.ts
export interface Shop {
  name: string;
}
```

そして pages

```pages/index.vue
<template>
  <div>
    {{ shopOptions }}
  </div>
</template>

<script lang="ts">
import Vue from 'vue';
import { mapGetters } from 'vuex';
import { Shop } from '~/types';

export default Vue.extend({
  computed: {
    ...mapGetters({
      shopOptions: 'shops/values',
    }),
  },
  mounted() {
    this.$store.dispatch('shops/fetch');
  },
});
</script>
```

今回問題となっている store
fetch を実行すると、サーバーからファイルを取得し、values に保持します。
clear を実行すると、保持していた values を clear します。

```store/shops.ts
import { Shop } from "~/types";
import { GetterTree } from "vuex/types/index";
import { RootState } from "~/store/types";

export const types = {
  SET: 'SET',
  CLEAR: 'CLEAR',
};

interface State {
  values: Shop[];
}

export const state: () => State = () => ({
  values: [],
});
    
export const mutations = {
  [types.SET](state: State, shops: Shop[]): void {
    state.values = shops;
  },
  [types.CLEAR](state: State): void {
    state.values = [];
  },
};

export const actions = {
  async fetch({ commit, getters }): Promise<void> {
    if (getters.values.length > 0) {
      return;
    }
    const { data } = await this.$axios.get('/shops');
    commit(types.SET, data);
  },

  clear({ commit }): void {
    commit(types.CLEAR);
  },
};

export const getters: GetterTree<State, RootState> = {
  values(state: State): Shop[] {
    return state.values;
  }
};
```

以上の 3ファイルになります。

上記のコードだと、 component 側で typo しても、なんのエラーも出ません。

```pages/index.vue
  mounted() {
      this.$store.dispatch('shops/fech'); // typo
  },
</script>
```

これを typescript のエラーとして警告してくれるのがゴールです。


## 公式の見解

[Nuxt TypeScript](https://typescript.nuxtjs.org/ja/cookbook/store.html#%E3%82%AF%E3%83%A9%E3%82%B9%E3%83%99%E3%83%BC%E3%82%B9) では、以下の選択肢が与えられています

* vanilla (今回は触れません)
* vuex-module-decorators
* vuex-class-component

ググってみると、`vuex-module-decorators` がベストのような[記事](https://qiita.com/suzukenz/items/0b5c0eb5196646599377)がありますが、
`vuex-class-component` もいいよ！みたいな[記事](https://qiita.com/hikaruna/items/fff8ed49556e659b9a06)もあったので、試してみました。


## vuex-module-decorators で型付け

まずは `vuex-module-decorators` で型付けをするやり方を見ていきます。

いつも通りライブラリをインストールします。

```bash
yarn add vuex-module-decorators
```

store を直していきます。
axios は後ほどやるので、ひとまずは仮データで進めます。

```store/shops.ts
import {
  Module,
  VuexModule,
  Mutation,
  Action,
} from "vuex-module-decorators";
import { Shop } from "~/types";

@Module({
  name: 'shops',
  stateFactory: true,
  namespaced: true,
})
export default class ShopsStore extends VuexModule {
  private shops: Shop[] = [];

  @Mutation
  private SET(shops: Shop[]): void {
    this.shops = shops;
  }

  @Mutation
  private CLEAR(): void {
    this.shops = [];
  }

  @Action({})
  public async fetch(): Promise<void> {
    if (this.shops.length > 0) {
      return;
    }
    const data = [{ name: 'tokyo' }];
    this.SET(data);
  }

  @Action({})
  public clear(): void {
    this.CLEAR();
  }

  public get values(): Shop[] {
    return this.shops;
  }
}
```

State は内部変数として書きます。`private` をつけると直接参照したときに Typescript エラーを起こしてくれます。
getter は `public get` のメソッドとして書き直します。

次に Initialize plugin を設定していきます。公式で書かれている内容なのでさらっといきます。

```store/index.ts
import { Store } from 'vuex';
import { initializeStores } from '~/utils/store-accessor';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const initializer = (store: Store<any>): void => initializeStores(store);

export const plugins = [initializer];
export * from '~/utils/store-accessor';
```

```utils/store-accessor.ts
import { Store } from 'vuex';
import { getModule } from 'vuex-module-decorators';
import ShopsStore from '~/store/shops';

let shopsStore: ShopsStore;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function initializeStores(store: Store<any>): void {
  shopsStore = getModule(ShopsStore, store);
}

export { initializeStores, shopsStore };
```

設定した store を読み込むように pages 側を変更します。

```pages/index.vue
<script lang="ts">
+ import { shopsStore } from '~/store';

(中略)

   computed: {
-    ...mapGetters({
-      shopOptions: 'shops/values',
-    }),

+   shopOptions(): Shop[] {
+     return shopsStore.values;
+   },
  },

  mounted() {
-   this.$store.dispatch('shops/fetch');
+   shopsStore.fetch();
  },
</script>
```

ここまでの変更で、typescript error が出るようになります。

```pages/index.vue
<script lang="ts">
(中略)

  mounted() {
    shopsStore.fech(); // typo
  },
</script>
```

```error
180:24 Property 'fech' does not exist on type 'ShopsStore'. Did you mean 'fetch'?
  > 180 |       await shopsStore.fech();
        |                        ^
```


以上で vuex-module-decorators による型付けは成功しました。
axios がまだ設定できていないので、そちらは[おまけ](#axios-の設定(おまけ))でやっていきます。


## vuex-class-component で型付け

`vuex-class-component` で型付けをするやり方を見ていきます。

ライブラリをインストールします。

```bash
yarn add vuex-class-component
```

store を直していきます。

```store/shops.ts
import {
  createModule,
  mutation,
  action,
  createProxy,
  extractVuexModule,
} from "vuex-class-component";
import Vuex from 'vuex';
import Vue from 'vue';
import { $axios } from '~/utils/api';
import { Shop } from "~/types";

Vue.use(Vuex);

const VuexModule = createModule({
  namespaced: 'shops',
  strict: false,
  target: 'nuxt',
});

export class ShopsStore extends VuexModule {
  private shops: Shop[] = [];

  @mutation
  private SET(shops: Shop[]): void {
    this.shops = shops;
  }

  @mutation
  private CLEAR(): void {
    this.shops = [];
  }

  @action
  public async fetch(): Promise<void> {
    if (this.shops.length > 0) {
      return;
    }
    const data = [{ name: 'tokyo' }];
    this.SET(data);
  }

  @action
  public async clear(): Promise<void> {
    this.CLEAR();
  }

  public get values(): Shop[] {
    return this.shops;
  }
}

const store = new Vuex.Store({
  modules: {
    ...extractVuexModule(ShopsStore),
  }
});

export const shopsStore: ShopsStore = createProxy(store, ShopsStore);
```

`vuex-module-decorators` と似ていますが、Module 指定のや store 登録周りの書き方が違います。
また、`vuex-module-decorators` とは違い、initialize plugin にあたる処理を各 store の中でやっています。

設定した store を読み込むように pages 側を変更します。

```pages/index.vue
<script lang="ts">
import { shopsStore } from '~/store/shops';

(中略)

   computed: {
-    ...mapGetters({
-      shopOptions: 'shops/values',
-    }),

+   shopOptions(): Shop[] {
+     return shopsStore.values;
+   },
  },

  mounted() {
-   this.$store.dispatch('shops/fetch');
+   shopsStore.fetch();
  },
</script>
```

vuex-module-decorators の時と同じように、typescript error が出るようになります。

```pages/index.vue
<script lang="ts">
(中略)

  mounted() {
    shopsStore.fech(); // typo
  },
</script>
```

```error
180:24 Property 'fech' does not exist on type 'ShopsStore'. Did you mean 'fetch'?
  > 180 |       await shopsStore.fech();
        |                        ^
```

## vuex-class-component での注意点

```pages/index.vue
shopsStore.CreateProxy(~, ~)
```

このような形で、store.CreateProxy() を呼びましょうと書いてある記事が多かったのですが、これは古い readme に書かれていた内容のようです。(2019/12/12 時点)

## まとめ

調べてみる限り `vuex-module-decorators` が優勢のようですが、書いてみると `vuex-class-component` の方が変更ファイルも少なく、書きやすい印象がありました。
公式は `vuex-module-decorators` を推してるけど、`vuex-class-component` も悪くないよ！ という内容でした。

ご指摘あったらコメントください！

## axios の設定

上記のコードでは axios の設定ができていないので、やっていきます。
とはいえ、[公式](https://typescript.nuxtjs.org/ja/cookbook/store.html#%E3%82%AF%E3%83%A9%E3%82%B9%E3%83%99%E3%83%BC%E3%82%B9) にも書いてあるので、参考程度に載せておきます。
なお、vuex-module-decorators でも vuex-class-component でも同じ書き方で動きます。

```utils/api.ts
import { NuxtAxiosInstance } from '@nuxtjs/axios';

let $axios: NuxtAxiosInstance;

export function initializeAxios(axiosInstance: NuxtAxiosInstance): void {
  $axios = axiosInstance;
}

export { $axios };
```

```plugins/axios-accessor.ts
import { Plugin } from '@nuxt/types';
import { initializeAxios } from '~/utils/api';

export const accessor: Plugin = ({ $axios }): void => {
  initializeAxios($axios);
};

export default accessor;
```

```nuxt.config.js
  plugins: [
    '@/plugins/axios-accessor',
  ]
```

```store/shops.ts
import { $axios } from '~/utils/api';

(中略)

    const { data } = await $axios.get('/shops');
```

以上！