Problems encountered

State management and business logic invocation, for example

Vuex uses a uniform dispatch/commit method to trigger Action and Mutation, and if nested modules are used, Vuex also parses the namespace to find the correct Action/Mutation function.

Nice for components, but maybe a little painful for project maintenance. While changes to state are separated from the view logic and placed in the Store folder, for the component that triggers the Action, maintaining the signature and interface of the store part of the function has to be searched globally (thus prompting some best practices under Vuex, For example, use CAPITAL_SNAKE_CASE nomenclature for all action names, or extract the mutation-types.js file).

As the project progresses, whenever the Action changes (adding or subtracting parameters, changing type, etc.), it is used in 5 areas of the project, and you carelessly change only 4 areas, a bomb silently pops up in a corner that is not used and not covered by testing.

Within the limits of Javascript’s capabilities, naming conventions and careful (constantly searching and validating changes) work to circumvent the problem. This mental burden is entirely avoidable thanks to Typescript’s powerful type inference capabilities.

The solution

Vuex’s d.ts file provides some useful types, but it has a lot of any in it, and we can actually refine the type restrictions on top of that.

The key code

This file exports some types, as well as two higher-order functions, makeDispatcher and makeMutator, that require the type to be passed in, as shown in the examples below.

// vuex-util.ts
import { ActionContext, Store, Module } from 'vuex'

type DictOf<T> = {[key: string]: T }

export type ActionDescriptor = [any.any]

export type ModuleActions<Context, Descriptor extends DictOf<ActionDescriptor>> = {
  [K in keyof Descriptor]: (ctx: Context, payload: Descriptor[K][0]) = > Descriptor[K][1]}export type ModuleMutations<State, PayloadTree> = {
  [K in keyof PayloadTree]: (state: State, payload: PayloadTree[K]) = > any
}

function isStore(context: any) {
  return ('strict' in context)
}

export function makeDispatcher<Context extends ActionContext<any.any>, Descriptor extends DictOf<ActionDescriptor> > (ns? :string) {
  return <K extends keyof Descriptor>(
    context: Store<any> | Context,
    action: K,
    payload: Descriptor[K][0],
  ) => {
    const _context: any = context
    let actionName = action as string
    if (ns && isStore(context)) {
      actionName = `${ns}/${action}`
    }
    return _context.dispatch(actionName, payload)
  }
}


/** * when the module is namespaced, '$store.mit (mutation)' is not the same as' ctx.mit (mutation) 'in the action handler */
export function makeMutator<Context extends ActionContext<any.any>, MutationPayloadTree> (ns? :string) {
  return <K extends keyof MutationPayloadTree>(
    context: Store<any> | Context,
    mutation: K,
    payload: MutationPayloadTree[K],
  ) => {
    let mutationName = mutation as string
    if (ns && isStore(context)) {
      mutationName = `${ns}/${mutation}`
    }
    return context.commit(mutationName, payload)
  }
}
Copy the code

Use example, a Todo App

Export Vuex Module

Here is a list of the last exported Vuex Module declarations:

import { ActionContext, Module } from 'vuex'

export const VUEX_NS = 'todo'

/** This object receives type variables representing module state and rootState types */
type TodoContext = ActionContext<TodosState, GlobalState>

// ...

export default {
  namespaced: true, state: {... }, actions: ACTIONS, mutations: MUTATIONS, }as Module<TodosState, GlobalState>
Copy the code

Simple ACTIONS and MUTATIONS

Declare ACTIONS, very simply retrieve data from localStorage, and call SET_TODOS mutation:

type ActionDescriptors = {
  GET_USER_TODOS: [{}, void]}const ACTIONS: ModuleActions<TodoContext, ActionDescriptors> = {
  GET_USER_TODOS(ctx) {
    const todos = JSON.parse(localStorage.getItem('todoItems')) || []
    ctx.dispatch('SET_TODOS', { todos })
  },
}
Copy the code

Next, MUTAIONS will be implemented by declaring all Mutation parameters and using the tool type ModuleMutations to mark the mutations object that will be delivered to Vuex Module:

type MutationPayloads = {
  SET_TODOS: { todos: TodoItem[] }
}

const MUTATIONS: ModuleMutations<TodosState, MutationPayloads> = {
}
Copy the code

The TS compiler will cause an error because the ModuleMutations tool type requires that all keys in the second type parameter be included, and an empty Object does not implement the SET_TODOS method required by MutationPayloads. So TS Error is thrown.

 const  MUTATIONS:  ModuleMutations<TodosState,  MutationPayloads> 

'MUTATIONS' is declared but its value is never read.ts(6133)

Property 'SET_TODOS' is missing in type '{}' but required in type 'ModuleMutations<TodosState, MutationPayloads>'.ts(2741)
Copy the code

Here’s a look at the smooth editor prompt experience:

Once MutationPayloads have been declared, a utility function can be used to generate a new commit-like function:

/** * A function with a type variable, using the example todoItemMutate(actionContext, 'mutationName', mutationPayload) */
export const todoItemMutate = makeMutator<TodoContext, MutationPayloads>(VUEX_NS)
Copy the code

Let’s change the way GET_USER_TODOS commits mutation and enjoy the code hint:

Ok, now you can use another utility function to generate a new dispatch-like function with type constraints and call it elsewhere:

/** * A function with a type variable, using the example todoItemDispatch(actionContext, 'actionName', actionName) */
export const todoItemDispatch = makeDispatcher<TodoContext, ActionDescriptors>(VUEX_NS)

...

todoItemDispatch(store, 'GET_USER_TODOS', {})
Copy the code

Then the requirements changed!

At this point you decide TodoApp needs to be able to support multiple users, each with their own record. So the ACTIONS interface specification needs to be changed:

type ActionDescriptors = {
  GET_USER_TODOS: [{ userName: string }, void]
}
Copy the code

The compiler will now report an error at the todoItemDispatch call at the end of the previous section:

Argument of type '{}' is not assignable to parameter of type '{ userName: string; } '.
  Property 'userName' is missing in type '{}' but required in type '{ userName: string; } '.ts(2345) todo.ts(., .) :'userName' is declared here.
Copy the code

Ok, now happily go to what went wrong and fix it:

todoItemDispatch(store, 'GET_USER_TODOS', { userName: 'hikerpig' })
Copy the code

No global search, no unnecessary parameter checking unit tests.

conclusion

advantages

  • Optimize the development experience
  • It does not change the way the actions and Mutations member functions are implemented, nor does vuex-typescript introduce some additional classes

disadvantages

  • The type of interface description needs to be declared separately in advance, so the function signature cannot be used directly
  • There is no way to combine all type enhancements into one unified onedispathFunction. Code in other files must be explicitly imported to take advantage of this type enhancementtodoItemDispatch/todoItemMutateMethod, slightly troublesome