VuexをTypeScriptで利用するのに悩んだ

## 概要

Vue.jsをTypeScriptで開発する際にVuexを利用するのにしっくりくる実装方法を模索中で、いくつか方法を試してみました。

GitHubに利用したプロジェクトをUPしています。実際に試してみたい方どうぞ^^

https://github.com/kai-kou/vue-js-typescript-vuex


## 準備

ここではDockerを利用して環境構築していますが、ローカルで構築してもらってもOKです。

```sh
> mkdir 任意のディレクトリ
> cd 任意のディレクトリ
> vi Dockerfile
> vi docker-compose.yml
```

```txt:Dockerfile
FROM node:10.8.0-stretch

RUN npm install --global @vue/cli

WORKDIR /projects
```

```yaml:docker-compose.yml
version: '3'

services:
  app:
    build: .
    ports:
      - "8080:8080"
    volumes:
      - ".:/projects"
    tty: true
```

```sh
> docker-compose up -d
> docker-compose exec app bash
```

```sh:コンテナ内
> vue create app

Vue CLI v3.0.1
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, TS, Vuex, Linter, Unit
? Use class-style component syntax? Yes
? Use Babel alongside TypeScript for auto-detected polyfills? Yes
? Pick a linter / formatter config: TSLint
? Pick additional lint features: Lint on save
? Pick a unit testing solution: Mocha
? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? No
? Pick the package manager to use when installing dependencies: (Use arrow keys)
❯ Use Yarn
  Use NPM
```

```sh:コンテナ内
> cd app
> yarn serve
```

これで実装の準備が整いました。

## Vue-Cli標準

```vue create``` コマンドでプロジェクトを作成するとsrc直下にstore.tsが作成されているので、そこに実装をいれて利用するパターンです。

stateに```counter``` ってのを持っていて、それをインクリメントするアクションがあるだけです。

```ts:src/store.ts
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

interface State {
  conuter: number;
}

export default new Vuex.Store({
  state: {
    conuter: 0,
  } as State,
  getters: {
    getCounter: (state, getters) => () => {
      return state.conuter;
    },
  },
  mutations: {
    increment(state, payload) {
      state.conuter += 1;
    },
  },
  actions: {
    incrementAction(context) {
      context.commit('increment');
    },
  },
});

```

App.vueで使ってみます。
超適当ですが、画像にclickイベント定義して、HelloWorldコンポーネントでstateに定義している```counter``` を表示してます。

```vue:src/App.vue
<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png" @click="increment">
    <HelloWorld :msg="`Welcome to Your Vue.js + TypeScript App ${this.counter}`"/>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import HelloWorld from './components/HelloWorld.vue';

@Component({
  components: {
    HelloWorld,
  },
})
export default class App extends Vue {
  private get counter(): number {
    return this.$store.getters.getCounter();
  }

  private increment(): void {
    this.$store.dispatch('incrementAction');
  }
}
</script>

(略)
```

ブラウザで確認すると、画像をクリックするとcounterがインクリメントするのが、確認できます。

<img width="692" alt="スクリーンショット_2018-08-28_10_28_43.png" src="https://qiita-image-store.s3.amazonaws.com/0/48549/45b1032b-b9cc-2aad-828c-4299612029fc.png">


### 気になる点

実装はシンプルで良いのですが、規模が大きくなってくると、store.tsが肥大化してくるのが目に見えます。辛い。

```this.$store``` を利用するので、メソッド名などを間違えていても実行時にしかエラーにならないので、せっかくTypeScript使っているのになーです。

```ts:src/App.vue
export default class App extends Vue {
  private get counter(): number {
    return this.$store.getters.getCounter2(); // 実行時にエラー
  }

  private increment(): void {
    this.$store.dispatch('incrementAction2'); // 実行時にエラー
  }
}

```

## vuex-type-helperを利用する

下記記事で紹介されていたvuex-type-helperを利用してみます。

Vue.js + Vuex + TypeScript を試行錯誤してみた
https://logs.utahta.com/blog/2017/09/02/110000

ktsn/vuex-type-helper
https://github.com/ktsn/vuex-type-helper


vuex-type-helperとvuex-classってのを追加します。

```sh:コンテナ内
> yarn add vuex-type-helper vuex-class
```

storeの実装を追加します。ここではモジュール化してみます。

```sh
> mkdir -pv store2/modules
> touch store2/index.ts
> touch store2/modules/app.ts
```

```ts:store2/index.ts
import Vue from 'vue';
import Vuex from 'vuex';
import { app } from '@/store2/modules/app';

Vue.use(Vuex);

export default new Vuex.Store({
  modules: {
    app,
  },
});

```

```ts:store2/modules/app.ts
import Vuex, { createNamespacedHelpers } from 'vuex';
import { DefineActions, DefineGetters, DefineMutations } from 'vuex-type-helper';

export interface State {
  counter: number;
}

export interface Getters {
  counter: number;
}

export interface Mutations {
  increment: {};
}

export interface Actions {
  incrementAction: {};
}

export const state: State = {
  counter: 0,
};

export const getters: DefineGetters<Getters, State> = {
  counter: (state) => state.counter,
};

export const mutations: DefineMutations<Mutations, State> = {
  increment(state, {}) {
    state.counter += 1;
  },
};

export const actions: DefineActions<Actions, State, Mutations, Getters> = {
  incrementAction({ commit }, payload) {
    commit('increment', payload);
  },
};

export const {
  mapState,
  mapGetters,
  mapMutations,
  mapActions,
} = createNamespacedHelpers<State, Getters, Mutations, Actions>('app');

export const app = {
  namespaced: true,
  state,
  getters,
  mutations,
  actions,
};

```

利用できるようにmain.tsとApp.vueを編集します。

```ts:src/main.ts
import Vue from 'vue';
import App from './App.vue';
-import store from './store';
+import store from './store2';

```

```ts:src/App.vue
import { Component, Vue } from 'vue-property-decorator';
import HelloWorld from './components/HelloWorld.vue';

import { Getter } from 'vuex-class';
import * as app from './store2/modules/app';

@Component({
  components: {
    HelloWorld,
  },
  methods: {
    ...app.mapActions(['incrementAction']),
  },
})
export default class App extends Vue {

  @Getter('app/counter') private counter!: number;
  private incrementAction!: (payload: {}) => void;
  private increment(): void {
    this.incrementAction({});
  }
}
(略)

```

はい。


## 気になる点

こちらの利点としてはモジュール化しやすい点と、アクション名を間違えてたときにビルドエラー吐いてくれる点でしょうか。


```ts:メソッド名間違い
  methods: {
    ...app.mapActions(['incrementAction2']),
  },
```

```txt:エラー出してくれる
Argument of type '"incrementAction2"[]' is not assignable to parameter of type '"incrementAction"[]'.
  Type '"incrementAction2"' is not assignable to type '"incrementAction"'.
```

getterで間違ってる場合は、ブラウザ側でエラーになります。惜しい。

```:プロパティ名間違い
  @Getter('app/counter2') private counter!: number;
```

ブラウザでエラー

<img width="1533" alt="スクリーンショット 2018-08-28 11.10.13.png" src="https://qiita-image-store.s3.amazonaws.com/0/48549/55cdad07-717f-99c3-5626-26238e6bd2ac.png">


vuex-classを利用するとActionなんかも以下のような定義ができるけれど、getterと同様にメソッド名間違いがブラウザでしか検知できないので、微妙。

```ts:vuex-classでAction定義
   @Action('app/incrementAction') private incrementAction!: (payload: {}) => void;
```

うーん。独自実装いれたらもっとブラウザエラーを回避できそうですが、どこまで実装しようか悩ましいところです。


Vue.js+TypeScriptで開発するときの参考記事まとめ
https://qiita.com/kai_kou/items/19b494a41023d84bacc7