Search code examples
typescriptvue.jsvisual-studio-codevuexintellisense

Have IDE suggest mutations and actions when commiting or dispatching


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?


Solution

  • 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