Vanilla VuexをTypeScriptで型安全に扱う

6 min読了の目安(約3600字TECH技術記事

はじめに

commitdispatch の第一引数が string のため,型情報が完全に失われてしまう.それにより,TypeScriptで書いていても型安全にすることはできず,書き心地がよくない.

本記事では, commit 等のラッパーを作ることで,型安全にそれらを使えるようにする方法を示す.

他の解決法

例えば,vuex-module-decoratorsというライブラリがある.しかし,nuxtでは当該ライブラリを使うにはclassic modeを使うしかなく,deprecatedである.また,decorator記法もexperimentalな機能のためプロジェクトによっては好まれないことも多い.

自分で型情報を作る

// state
import { ActionTree, Commit, CommitOptions, Dispatch, DispatchOptions, MutationTree, Store } from 'vuex'

const moduleName = 'sample'

// state
interface State {
  count: number
}

export const state = (): State => ({
  count: 0,
})

// mutations
const makeMutations = <T extends MutationTree<State>>(mutations: T): T => mutations

export const mutations = makeMutations({
  increment(state) {
    state.count++
  },

  incrementBy(state, payload: number) {
    state.count += payload
  },
})

// actions
const makeActions = <T extends ActionTree<State, unknown>>(actions: T): T => actions

export const actions = makeActions({
  increment({ commit }): void {
    _commit(commit, 'increment')
  },

  async incrementAsync({ commit }): Promise<number> {
    // mimic api
    const sleep = (t: number): Promise<number> => new Promise((resolve, reject) => setTimeout(() => resolve(t), t))

    const res = await sleep(1000)
    _commit(commit, 'incrementBy', res)
    return res
  },

  dispatchOtherAction({ dispatch }): void {
    _dispatch(dispatch, 'incrementAsync').then(v => {
      console.log(v)
    })
  },
})

// util-commit
type CommitPayload<K extends keyof typeof mutations> = Parameters<typeof mutations[K]>[1]
type CommitParameters<K extends keyof typeof mutations> = CommitPayload<K> extends undefined
  ? [K] | [K, CommitPayload<K>] | [K, CommitPayload<K>, CommitOptions]
  : [K, CommitPayload<K>] | [K, CommitPayload<K>, CommitOptions]

const _commit = <K extends keyof typeof mutations>(
  commit: Commit,
  ...[key, payload, options]: CommitParameters<K>
): void => commit(key, payload, options)

export const commit = <K extends keyof typeof mutations>(
  store: Store<any>,
  ...[key, payload]: CommitParameters<K>
): void => store.commit(`${moduleName}/${key}`, payload)

// util-actions
type DispatchPayload<K extends keyof typeof actions> = Parameters<typeof actions[K]>[1]
type DispatchParameters<K extends keyof typeof actions> = DispatchPayload<K> extends undefined
  ? [K] | [K, DispatchPayload<K>] | [K, DispatchPayload<K>, DispatchOptions]
  : [K, DispatchPayload<K>] | [K, DispatchPayload<K>, DispatchOptions]

const _dispatch = <K extends keyof typeof actions>(
  dispatch: Dispatch,
  ...[key, payload, options]: DispatchParameters<K>
): ReturnType<typeof actions[K]> => dispatch(`${moduleName}/${key}`, payload, options) as ReturnType<typeof actions[K]>

export const dispatch = <K extends keyof typeof actions>(
  store: Store<any>,
  ...[key, payload, options]: DispatchParameters<K>
): ReturnType<typeof actions[K]> =>
  store.dispatch(`${moduleName}/${key}`, payload, options) as ReturnType<typeof actions[K]>

まず, MutationTreeActionTree はkeyがstringとされているため,何もしない関数を通してkeyの情報を取り出せるようにしています(より良い方法があれば知りたいです).

それを利用して, dispatchcommit のラッパー関数を作ることで, commit の引数が string でしかないことを解消し,さらに payload にも型を与えられるようにしています.また, payload が必要かどうかも含めた型になっているので, incrementBycommit する際には payload を与えないと型エラーになるようになっています.

コンポーネント側での利用も同様に型情報付きで行えるようにしており,以下のように書くことができます.

import Vue from 'vue'
import * as SampleStore from '~/store/sample'

export default Vue.extend({
  mounted() {
    SampleStore.dispatch(this.$store, 'increment')
  }
})