vuex-simpleを使ってTypeScriptでvuexを書く

2018.11.27

TypeScriptでのvuexコーディングについてはまだデファクトスタンダードな書き方が無い。2018/10/4に出来たばかりの`vuex-simple`が書きやすそうだったので試した記録。
https://github.com/sascha245/vuex-simple

# 前提

vue-cli(^3.0.0)によりプロジェクトをsetupしてます。

```bash
$ vue create ts-project
# 色々聞かれるけど、vue-router, vuex, typescriptあたりを有効にする

# "vue": "^2.5.17",
# "vue-router": "^3.0.1",
# "vuex": "^3.0.1",
# "vuex-simple": "^2.0.1"
```

# 使い方

## インストール

普通

```bash
$ npm i vuex-simple
# or
$ yarn add vuex-simple
```

## storeの書き方

とてもわかりやすい。vuex使ったことある人ならコード読めば何やってるかわかる。

```ts:store/sample.ts
// 適当なvuex module定義
import { Getter, State, Mutation, Action } from 'vuex-simple';

export class SampleModule {
  @State() public param1: int = 3;
  @State() public param2: string | null = null;

  @Mutation()
  public setParam1(param: int) {
    this.param1 = param;
  }

  @Getter()
  public get param1x3() { return this.param1 * 3; }

  @Action()
  public async increment1() {
    await new Promise(r => setTimeout(r, 200));
    this.setParam1(this.param1 + 1);
  }

  // @Module()でnested moduleにも対応できる
}
```

```ts:store/index.ts
// moduleをまとめ上げて、vuex-simpleで初期化
import Vue from 'vue';
import Vuex from 'vuex';

import { State, Module, createVuexStore, Mutation } from 'vuex-simple';
import { SampleModule } from './sample';

// 　まとめのstoreクラス
export class VStore {
  @Module() public sample = new SampleModule();

  @State() public hoge = 'xxxxx';

  // v2.0.1ではrootモジュールのmutation/action/getterは動かない（直接vuexでmutation操作は可能）
  @Mutation()
  public setHoge(hoge: string) {
    this.hoge = hoge;
  }
}

// vuex初期化
Vue.use(Vuex);

const isDev = process.env.NODE_ENV === 'development';
const store = new VStore();
export default createVuexStore(store, {
  // 開発中はtrue, 本番はfalseにする
  strict: isDev,
  modules: {},
  plugins: []
});
```

## Componentでの利用

```vue:views/Home.vue
<template>
  <div>
    <div>store.sample.param1 is "{{ store.sample.param1 }}"</div>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { useStore } from 'vuex-simple';
import { VStore } from '@/store';

@Component({})
export default class Home extends Vue {
  public store: VStore = useStore(this.$store);
}
</script>

```


# Tips的なもの

## template内でstoreに直接アクセスしたくない

componentで`computed`プロパティを作れば良いかな。

```ts
@Component({})
export default class Home extends Vue {
  public store: VStore = useStore(this.$store);

  public get sample() { return this.store.sample }
  public get sampleParam1() { return this.store.sample.param1 }
}
```

あるいは、次項のvuex-typediを使ってDIすれば良い。

## vuex-typediについて

vuex-simpleと同じ作者が作った、`vuex-typedi`パッケージを使って、依存性注入することが出来る。
https://github.com/sascha245/vuex-simple#example-with-dependency-injections
https://github.com/sascha245/vue-typedi

手間の割にメリットが少ないような気がするなあ。プロジェクト全体でtypediを使ってるなら有用なのか？あとはsubmoduleを直接DIできるので不必要なmoduleに触らせたくない・規模の大きめなプロジェクトだと有用とか？


## stateがpublicなのが嫌だ。外からはreadonlyにしたい

vuexの`strict`を開発時に`true`にしてれば、実行時にエラー吐いてくれるので、基本的にはpublicのままで対応出来る。

どうしてもprivateにしたいなら

```ts
export class SampleModule {
  @State() private _param1: int = 3;

  @Mutation()
  public setParam1(param: int) {
    this._param1 = param;
  }

  @Getter()
  public param1() { return this._param1; }

  @Action()
  public async increment1() {
    await new Promise(r => setTimeout(r, 200));
    this.setParam1(this.param1 + 1);
  }
}
```

みたいにすればまあいけるのかな。なんか昔のphpぽい。
とはいえ実行時には`_param1`にアクセスできる（TypeScriptの仕様）のであまり意味はない。


## Nuxt.jsで使いたい

nuxtが勝手にvuexの初期化をしてくれる`モジュールモード`でなく、`クラシックモード`を使えば良さそう。（未検証）
https://ja.nuxtjs.org/guide/vuex-store/

```ts:store/index.js
import { createVuexStore } from 'vuex-simple';

// ~~~ 省略

const createStore = () => {
  const isDev = process.env.NODE_ENV === 'development';
  return createVuexStore(store, {
    strict: isDev,
    modules: {},
    plugins: []
  });
}

export default createStore
```

## action, mutationのpayloadに型を持たせたい

payloadの型を定義してメソッドの引数で指定すればok

```ts:store/sample.ts
export interface MSetHogePayload {
  hoge: int;
  additionalHoge: int;
}
export interface AUpdateHogePayload {
  hoge: int;
}

export class SampleModule {
  @State() public hoge: int = 3;
  @Mutation()
  public setHoge(payload: MSetHogePayload) {
    this.hoge = payload.hoge + payload.additionalHoge;
  }
  @Action()
  public async updateHoge(payload: AUpdateHogePayload) {
    await new Promise(r => setTimeout(r, 200));
    this.setHoge({ hoge: payload.hoge, additionalHoge: 100 });
  }
}
```

```ts:views/Home.vue

@Component({})
export default class Home extends Vue {
  public store: VStore = useStore(this.$store);

  public mounted() {
    // payload interfaceの定義を満たしてればOK
    this.store.sample.updateHoge({ hoge: 2 }); // OK
    this.store.sample.updateHoge({ hoge: "string" }); // NG

    // またはimport {AUpdateHogePayload} from '@/store/sample'して明示的に型変換しても
    this.store.sample.updateHoge({ hoge: 3 } as AUpdateHogePayload);
  }
}
```


# 所感

名前の通りsimpleで使いやすい。

他のライブラリだと、vuex-classはcomponentへのbinding helperなので、vuex自体については別途考える必要があるし、vuex-type-helperはおまじないが多くてつらかった。

component側での利用時はvuex-classを使えそうな感じがするので（未検証）、プロジェクトにより使ったら良いのかな。