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?
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.
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
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>
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).
This was all done on vs-code
with the following packages:
typescript@^4.5.5
vuex@^4.0.2
vue@^3.2.31