Vuex4 + Typescript

A complete demo of Vuex4 + Typescript with full set of type check and module support.

· 3 min read
Vuex4 + Typescript

Background

Writing Vuex in Typescript can be pretty complex, even with Vuex4. Just as you can see in Vuex5 RFC, you can easily understand what's the drawbacks of Vuex4.

Here is a complete demo of Vuex4 + Typescript with full set of type check and module support, who handles JAVA Spring Boot's dictionary.

States

// state.ts
export interface DictItem {
  label: string
  code: number
  sort: number
}
export interface DictState {
  status: {}
  data: {
    [key: string]: DictItem[]
  }
}

export const state: DictState = {
  status: {},
  data: {}
}

Define Constants

// mutation-types.ts
export enum DictMutationType {
  SET_FIELD = 'SET_FIELD',
  SET_STATUS_PENDING = 'SET_STATUS_PENDING',
  SET_STATUS_READY = 'SET_STATUS_READY',
  SET_STATUS_ERROR = 'SET_STATUS_ERROR'
}
// action-types
export enum DictActionType {
  ACTION_GET_DICT = 'ACTION_GET_DICT'
}

Mutations

// mutations.ts
import { MutationTree } from 'vuex'
import { DictState, DictItem } from './state'
import { DictMutationType } from './mutation-types'

export type Mutations<S = DictState> = {
  [DictMutationType.SET_FIELD](state: S, { namespace, data }: { namespace: string; data: DictItem[] }): void
  [DictMutationType.SET_STATUS_PENDING](state: S, namespace: string): void
  [DictMutationType.SET_STATUS_READY](state: S, namespace: string): void
  [DictMutationType.SET_STATUS_ERROR](state: S, namespace: string): void
}

export const mutations: MutationTree<DictState> & Mutations = {
  [DictMutationType.SET_FIELD](state: DictState, { namespace, data }) {
    state[namespace] = data
  },
  [DictMutationType.SET_STATUS_PENDING]: (state, namespace) => {
    state.status[namespace] = 'PENDING'
  },
  [DictMutationType.SET_STATUS_READY]: (state, namespace) => {
    state.status[namespace] = 'READY'
  },
  [DictMutationType.SET_STATUS_ERROR]: (state, namespace) => {
    state.status[namespace] = 'ERROR'
  }
}

Actions

// actions.ts
import { ActionTree, ActionContext } from 'vuex'
import { cloneDeep } from 'lodash'
import { RootState } from '@/store'
import type { DictState, DictItem } from './state'
import { Mutations } from './mutations'
import { DictMutationType } from './mutation-types'
import { DictActionType } from './action-types'
import { getDicts } from '@/apis/system/dict'

type AugmentedActionContext = {
  commit<K extends keyof Mutations>(key: K, payload: Parameters<Mutations[K]>[1]): ReturnType<Mutations[K]>
} & Omit<ActionContext<DictState, RootState>, 'commit'>

export interface Actions {
  [DictActionType.ACTION_GET_DICT](
    { commit }: AugmentedActionContext,
    {
      namespace,
      withAny,
      anyLabel,
      anyCode,
      anySort
    }: { namespace: string; withAny?: boolean; anyLabel?: string; anyCode?: number; anySort?: number }
  ): Promise<DictItem[]>
}

export const actions: ActionTree<DictState, RootState> & Actions = {
  [DictActionType.ACTION_GET_DICT](
    { commit, state }: AugmentedActionContext,
    { namespace, withAny = false, anyLabel = '全部', anyCode = -1, anySort = 0 }
  ) {
    const anyObj = { label: anyLabel, code: anyCode, sort: anySort }
    if (state.data[namespace]) {
      const _state = cloneDeep(state.data[namespace])
      if (withAny) _state.unshift(anyObj)
      return Promise.resolve(_state)
    }

    if (state.status[namespace] === 'PENDING') {
      return new Promise((resolve, reject) => {
        const timer = setInterval(() => {
          if (state.status[namespace] === 'READY') {
            const _state = cloneDeep(state.data[namespace])
            if (withAny) _state.unshift(anyObj)
            resolve(_state)
            clearInterval(timer)
          } else if (state.status[namespace] === 'ERROR') {
            reject(Error('获取字典失败'))
          }
        }, 10)
      })
    }

    commit(DictMutationType.SET_STATUS_PENDING, namespace)
    return getDicts(namespace)
      .then((res) => {
        const data = res.data.data.map((item) => ({ label: item.dictLabel, code: item.dictCode, sort: item.dictSort }))
        commit(DictMutationType.SET_FIELD, { namespace, data })
        commit(DictMutationType.SET_STATUS_READY, namespace)
        if (withAny) data.unshift(anyObj)
        return Promise.resolve(data)
      })
      .catch((err) => {
        commit(DictMutationType.SET_STATUS_ERROR, namespace)
        return Promise.reject(err)
      })
  }
}

Handle modules

We will separete content above as modules for better management.

// store.ts
import { createStore, createLogger } from 'vuex'
// import createPersistedState from 'vuex-persistedstate'
import { store as dict, DictStore, DictState } from '@/store/modules/dict'

export interface RootState {
  dict: DictState
}

// Attach ahother store type here with & symbol
export type Store = DictStore<Pick<RootState, 'dict'>>

// Plug in logger when in development environment
const debug = process.env.NODE_ENV !== 'production'
const plugins = debug ? [createLogger({})] : []
// Plug in session storage based persistence
// plugins.push(createPersistedState({ storage: window.sessionStorage }))

export const store = createStore({
  plugins,
  modules: {
    dict
  }
})

export function useStore(): Store {
  return store as Store
}

Now export our moudule to store

// index.ts
import { Store as VuexStore, CommitOptions, DispatchOptions, Module } from 'vuex'

import { RootState } from '@/store'
import { mutations, Mutations } from './mutations'
import { actions, Actions } from './actions'
import type { DictState } from './state'
import { state } from './state'

export { DictState }

export type DictStore<S = DictState> = Omit<VuexStore<S>, 'getters' | 'commit' | 'dispatch'> & {
  commit<K extends keyof Mutations, P extends Parameters<Mutations[K]>[1]>(
    key: K,
    payload: P,
    options?: CommitOptions
  ): ReturnType<Mutations[K]>
} & {
  dispatch<K extends keyof Actions>(
    key: K,
    payload: Parameters<Actions[K]>[1],
    options?: DispatchOptions
  ): ReturnType<Actions[K]>
}
export const store: Module<DictState, RootState> = {
  state,
  mutations,
  actions
  // We use global action type constants, so no need to enable namespacing
  // namespaced: true,
}

Wow, it finally works! But I have to say I spend three more times of time to write types than in Javascript, does it worthy? Maybe...

Related Articles

Element UI Multi Upload Component
· 2 min read