vuex-aggregate で map系ヘルパーも守る

# 🎉 mapXXX系に型が来た

筆者の開発した [vuex-aggregate](https://github.com/takefumi-yoshii/vuex-aggregate) が v4 になりました。標準的な SFC をTypeScriptで強化できます。以下が最小利用例で、「これのどこが TypeScript？」と感じるかもしれません。**import している Root は型付け済み**で、定義元の rootState.name を名称変更すると、SFC の mapヘルパー文字列参照までもエラーが発生します。vuex-aggregate が守っているのは「リファクタによる不整合事故」です。型付けられたmapState は `count` と `name` の文字列しか受け入れなくなります。

```app.vue
<script lang='ts'>
import Vue from 'vue'
import * as Root from '../store/root'
const computed = {
  ...Root.mapState(['name']),
  ...Root.mapState({ rootName: 'name' }),
  ...Root.mapState({ rootNameLabel: state => `root is ${state.name}` }),
}
```
3つ目の書き方でバインドされる state引数 にも、 RootState の型がついています。Rootはこの様な定義です。

```root.ts
import { fromState } from 'vuex-aggregate'

interface State {
  count: number
  name: string
}
const state: State = {
  count: 0,
  name: 'unknown'
}
export const namespace = ''
export const { mapState } = fromState(state, namespace)
```
`fromState` で RootStateのみを知っている Vuex.mapState を生成します。Vuex が提供しているそれにプロキシを通しつつ、名前空間を解決し「型」をかけています。これだけで型がつくので、TypeScript 導入のハードルも下がるかと思います。

以下はリポジトリのexampleです。まずは vscode 等で example SFC を適当に壊してみてください。開発サーバーを立てるまでもなく、怒られると思います。また、定義したメソッドや state が即座にコードヒントに出てくるので、DX がかなり上がるかと思います。

```app.vue
<template>
  <div>
    <p>rootName = {{rootName}}</p>
    <p>rootNameLabel = {{rootNameLabel}}</p>
    <p>nestedModuleName = {{nestedModuleName}}</p>
    <p>nestedModuleNameLabel = {{nestedModuleNameLabel}}</p>
    <p>count = {{count}}</p>
    <p>double = {{double}}</p>
    <p>expo2 = {{expo2}}</p>
    <p>countLabel = {{countLabel}}</p>
    <p>isRunningAutoIncrement = {{autoIncrementLabel}}</p>
    <p>name = {{nameLabel}}</p>
    <div>
      <button @click="increment()">+1</button>
      <button @click="decrement()">-1</button>
      <button @click="asyncIncrement()">asyncIncrement</button>
      <button @click="toggleAutoIncrement(100)">toggleAutoIncrement</button>
    </div>
    <div>
      <input @change="e => setName(e.target.value)" />
    </div>
  </div>
</template>

<script lang='ts'>
import Vue from 'vue'
import * as Root from '../store/root'
import * as Counter from '../store/modules/counter'
import * as NestedModule from '../store/modules/nested/module'
const computed = {
  ...Root.mapState({ rootName: 'name' }),
  ...Root.mapGetters({ rootNameLabel: 'nameLabel' }),
  ...NestedModule.mapState({ nestedModuleName: 'name' }),
  ...NestedModule.mapGetters({ nestedModuleNameLabel: 'nameLabel' }),
  ...Counter.mapState(['count']),
  ...Counter.mapState({ double: state => state.count * 2 }),
  ...Counter.mapGetters(['nameLabel', 'autoIncrementLabel']),
  countLabel() {
    return Counter.getters.countLabel('pt')
  },
  expo2() {
    return Counter.getters.expo(2)
  }
}
const methods = {
  ...Counter.mapMutations(['increment', 'decrement']),
  ...Counter.mapActions(['asyncIncrement']),
  setName(value: string) {
    Counter.commits.setName(value)
  },
  toggleAutoIncrement(duration: number) {
    Counter.dispatches.toggleAutoIncrement({ duration })
  }
}
export default Vue.extend({ computed, methods })
</script>
```

# 🤔 何をやっているのか

Vuex は Flux と mvvm の良いところ取りをしている訳ですが、モジュールが分散することにより、それぞれの存在や構造を知ることは出来ないため、型定義は今まで困難でした。

vuex-aggregate では、TypeScript が型解析出来る体系を module 実装に要求していますが、今までの実装とさほど差異はありません。定義例は以下の通りです。

```counter.ts
import {
  fromState,
  fromMutations,
  fromActions,
  fromGetters,
  Injects,
  StateFactory
} from 'vuex-aggregate'
import { wait } from '../../utils/promise'
import * as Root from '../root'

// ____________________________________
//
// @ State

const namespace = 'counter'

interface State {
  count: number
  name: string
  isRunningAutoIncrement: boolean
}
const state: State = {
  count: 0,
  name: 'unknown',
  isRunningAutoIncrement: false
}
const stateFactory: StateFactory<State> = injects => ({ ...state, ...injects })
const { mapState } = fromState(state, namespace)

// ____________________________________
//
// @ Getters

const _getters = {
  nameLabel(
    state: State,
    getters: any,
    rootState = Root.state, // initial props for InferredType
    rootGetters = Root.getters // initial props for InferredType
  ): string {
    console.log(`root name is ${rootState.name}. rootGetters nameLabel = ${rootGetters.nameLabel}`)
    return `my name is ${state.name}`
  },
  autoIncrementLabel(state: State): string {
    const flag = state.isRunningAutoIncrement
    return flag ? 'true' : 'false'
  },
  countLabel(state: State): (unit: string) => string {
    return unit => {
      return `${state.count} ${unit}`
    }
  },
  expo(state: State): (amount: number) => number {
    return amount => {
      return state.count ** amount
    }
  }
}
const { getters, mapGetters } = fromGetters(_getters, namespace)

// ____________________________________
//
// @ Mutations

const mutations = {
  increment(state: State): void {
    state.count++
  },
  decrement(state: State): void {
    state.count--
  },
  setCount(state: State, count: number): void {
    state.count = count
  },
  setName(state: State, name: string): void {
    state.name = name
  },
  setRunningAutoIncrement(state: State, flag: boolean): void {
    state.isRunningAutoIncrement = flag
  }
}
const { commits, mutationTypes, mapMutations } = fromMutations(mutations, namespace)

// ____________________________________
//
// @ Actions

const actions = {
  async asyncIncrement() {
    await wait(1000)
    commits.increment()
  },
  async toggleAutoIncrement(
    { state }: { state: State },
    { duration }: { duration: number }
  ) {
    const flag = !state.isRunningAutoIncrement
    commits.setRunningAutoIncrement(flag)
    while (true) {
      if (!state.isRunningAutoIncrement) break
      await wait(duration)
      commits.increment()
    }
  }
}
const { dispatches, actionTypes, mapActions } = fromActions(actions, namespace)

// ____________________________________
//
// @ ModuleFactory

const moduleFactory = (injects?: Injects<State>) => ({
  namespaced: true, // Required
  state: stateFactory(injects),
  getters: _getters,
  mutations,
  actions,
  // modules: Don't use nested modules. if you need them, resolve in file namespace.
})

export {
  State,
  namespace,
  moduleFactory,
  mutationTypes,
  actionTypes,
  getters,
  commits,
  dispatches,
  mapState,
  mapGetters,
  mapMutations,
  mapActions
}
```

純粋な javascript 実装で言うと、各々コールした from 関数内部の clojur にエイリアス文字列を確保したり、vuex-aggregate 内部にアサインした store インスタンス・mapHelper 各種を叩いているだけです。`VuexAggregate.use(store)` で、store 定義直後にインスタンスをアサインします。

```store.ts
Vue.use(Vuex)
const store = new Vuex.Store({
  ...rootFactory({ name: 'ROOT' }),
  modules: {
    [Counter.namespace]: Counter.moduleFactory({ name: 'COUNTER' }),
    [NestedModule.namespace]: NestedModule.moduleFactory({ name: 'NESTED_MODULE' })
  }
})
VuexAggregate.use(store) // Required
export { store }
```

# 📝 gettersメソッドの引数ついて

getters の関数は第4引数までとれますが、example は以下の様になっています。

```counter.ts
nameLabel(
  state: State,
  getters: any,
  rootState = Root.state
  rootGetters = Root.getters
): string {
```
第2引数は関数が属する getters を指しますが、ここの推論は不可能です。もしここに型が必要な場合は、interface Getters を定義し、それに沿った _getters を定義してください。第3・第4引数は、default option に Root を当てることで、引数から推論型を得ることが出来ます。


# 終わりに

vuex-aggregate は TypeScript2.8 の新機能で出来る様になったところが随所にあり、現状で基本的な機能は型で守れてると思います。引数が必要な関数は、定義元の引数型変更を追従出来ないので、mapヘルパー利用は避けましょう。

scripts と template の間で型の疎通はもちろん出来ないので、そこまでシビアになりたい人は、もう React 使った方が要件にあっていると思います。

また、これは外から型をつけるタイプのモジュールであり、ドキュメントをなぞって推論導出する様に型定義を書いています。筆者は Vuex の内部実装に詳しい訳ではありませんので、最新の Vuex と乖離する可能性も把握した上で利用してください。

Nuxt などではまだ試していないので、create module がもしかしたら影響を与えるかもしれませんが、その時はまた考えます。