Have IDE suggest mutations and actions when commiting or dispatching

Viewed 146

Currently I am using Vuex in a Vue 3 Typescript project. I have something like this:

import { createStore, useStore as baseUseStore, Store } from 'vuex';
import { InjectionKey } from 'vue';


export interface State {
    foo: string;
  }
  
  export const key: InjectionKey<Store<State>> = Symbol();
  
  export const store = createStore<State>({
      state: {foo: 'foo'},
      mutations: { 
        changeFoo(state: State, payload: string){
            state.foo = payload
        }
      },
      actions: { 
        setFooToBar({commit}){
         commit('changeFoo', 'bar')
      }}
  })

  export function useStoreTyped() {
    return baseUseStore(key);
  }
  
  

Then later in a component I type out:

import { useStoreTyped } from "../store";

const store = useStoreTyped();

function() {
   store.distpatch('... // at this point I would like to see a list of my actions
}

This setup is nice because in my IDE it if I start typing store.state. VS Code will popup a little box that suggests the props on my state object (in this example .foo). However I am not getting the same behavior when I try and commit mutations or dispatch actions. How can I give the vuex store object the mutation and action names so it can provide suggestions (intellisense) for these?

1 Answers

Can you add types? Yes you can. Whether or not you should add types (instead of waiting for vuex to add better types) is a different question.

Basic Solution

Here is a way to add type hints to the commit and dispatch methods of the store instance:

import { createStore, useStore as baseUseStore, Store, ActionHandler, DispatchOptions, CommitOptions } from 'vuex';
import { InjectionKey } from 'vue';


export interface State {
    foo: string;
}

export const key: InjectionKey<Store<State>> = Symbol();

const storeInitializer = {
    state: { foo: 'foo' } as State,
    mutations: {
        changeFoo(state: State, payload: string) {
            state.foo = payload
        },
        rearrangeFoo(state: State) {
            state.foo = state.foo.split('').sort().join()
        }
    },
    actions: {
        setFooToBar: (({ commit }) => {
            commit('changeFoo', 'bar')
        }) as ActionHandler<State, State>
    }
}

export type TypedDispatchAndAction<T extends { mutations: any, actions: any }> = {
  dispatch: (type: keyof T['actions'], payload?: any, options?: DispatchOptions) => Promise<any>
  commit: (type: keyof T['mutations'], payload?: any, options?:  CommitOptions) => void
}

export const store = createStore<State>(storeInitializer)

export function useStoreTyped() {
    const keyedStore = baseUseStore(key);

    return keyedStore as typeof keyedStore & TypedDispatchAndAction<typeof storeInitializer>
}

This method has some downsides: It's not very graceful, and it doesn't do full type checking if a mutation or action requires a payload

Complex Solution

This solution adds type hints for the key parameter of both commit and dispatch as well as adding type checking for the payload parameter

import { createStore, useStore as baseUseStore, Store, DispatchOptions, CommitOptions, ActionContext } from 'vuex';
import { InjectionKey } from 'vue';

type Length<L extends any[]> = L['length']

type Function<P extends any[] = any, R extends any = any> = (...args: P) => R

type ParamLength<Fn extends Function> = Length<Parameters<Fn>>

type P1<Fn extends (...args: any) => any> = Parameters<Fn>[1]

type Payload<H extends Function> = ParamLength<H> extends 2
    ? P1<H> extends infer X ? X : never
    : never

type TypedDispatchOrCommit<A extends { [key: string]: Function }, Options, Return, K extends keyof A = keyof A> = {
    <I extends K>(key: I, ...args: {
        0: [],
        1: [payload: Payload<A[I]>]
    }[Payload<A[I]> extends never ? 0 : 1]): Return
    <I extends K>(key: I, ...args: {
        0: [payload: undefined, options: Options],
        1: [payload: Payload<A[I]>, options: Options]
    }[Payload<A[I]> extends never ? 0 : 1]): Return
}

export type TypedDispatchAndAction<T extends { mutations: any, actions: any }> = {
  dispatch: TypedDispatchOrCommit<T['actions'], DispatchOptions, Promise<any>>
  commit: TypedDispatchOrCommit<T['mutations'], CommitOptions, void>
}



export interface State {
    foo: string;
}

export const key: InjectionKey<Store<State>> = Symbol();

const storeInitializer = {
    state: { foo: 'foo' } as State,
    mutations: {
        changeFoo(state: State, payload: string) {
            state.foo = payload
        },
        rearrangeFoo(state: State) {
            state.foo = state.foo.split('').sort().join()
        }
    },
    actions: {
        setFooToBar({ commit }: ActionContext<State, State>) {
            commit('changeFoo', 'bar')
        },
        setFoo({ commit }: ActionContext<State, State>, payload: string) {
            commit('changeFoo', payload)
        }
    }
}

export const store = createStore<State>(storeInitializer)

export function useStoreTyped() {
    const keyedStore = baseUseStore(key);

    return keyedStore as Omit<typeof keyedStore, 'dispatch' | 'commit'> & TypedDispatchAndAction<typeof storeInitializer>
}

Despite its downside of not being very intelligible, the above code does pass the following tests:

import { useTypedStore } from '../store'

const store = useTypedStore()

// expected: Pass, actual: Pass, returns void
store.commit('rearrangeFoo')
// expected: Fail, actual: Fail, returns void
store.commit('changeFoo')
// expected: Pass, actual: Pass, returns void
//    also gives typehint of string for payload
store.commit('changeFoo', 'bar')

The same is true for dispatch, except it returns a Promise<any>

Conclusion

You can get types, but the best solution is for vuex to overhaul their types for better type hinting (assuming that fits their project vision).

Notes

This was all done on vs-code with the following packages:

typescript@^4.5.5
vuex@^4.0.2
vue@^3.2.31
Related