Vuex 4 Typescript Declarations Generator

Example/index.ts

import { InjectionKey } from 'vue'
import { CommitOptions, createStore, DispatchOptions, Store, useStore } from 'vuex'
import { mutations, ExampleMutations } from './mutations'
import { actions, ExampleActions } from './actions'
import { getters, ExampleGetters } from './getters'

// ----------------------------------------------------------------------------
// State
// ----------------------------------------------------------------------------

export interface ExampleState {
    count: number
}

export function createDefaultExampleState(): ExampleState {
    const defaultState: ExampleState = {
        count: 0,
    }

    return defaultState
}

// ----------------------------------------------------------------------------
// TypeScript Helpers
// ----------------------------------------------------------------------------

type TypedStore = Omit<Store<ExampleState>, 'commit' | 'dispatch' | 'getters'> & {
    commit<K extends keyof ExampleMutations>(
        key: K,
        payload?: Parameters<ExampleMutations[K]>[1],
        options?: CommitOptions
    ): ReturnType<ExampleMutations[K]>
} & {
    dispatch<K extends keyof ExampleActions>(
        key: K,
        payload?: Parameters<ExampleActions[K]>[1],
        options?: DispatchOptions
    ): ReturnType<ExampleActions[K]>
} & {
    getters: {
        [K in keyof ExampleGetters]: ReturnType<ExampleGetters[K]>
    }
}

export const injectionKeyExample: InjectionKey<TypedStore> = Symbol('Vuex (Example) InjectionKey')

export function useExampleStore(): TypedStore {
    return useStore(injectionKeyExample)
}

// ----------------------------------------------------------------------------
// Store
// ----------------------------------------------------------------------------

export function createExampleStore(): TypedStore {
    const store = createStore<ExampleState>({
        strict: true,
        state: createDefaultExampleState,
        mutations,
        actions,
        getters,
    }) as TypedStore

    return store
}

Example/mutations.ts

import { MutationTree } from 'vuex'
import { ExampleState } from '.'

// ----------------------------------------------------------------------------
// Interfaces
// ----------------------------------------------------------------------------

export enum ExampleMutation {
    INCREMENT = 'INCREMENT',
}

// ----------------------------------------------------------------------------
// Mutations
// ----------------------------------------------------------------------------

export interface ExampleMutations {
    [ExampleMutation.INCREMENT]: (state: ExampleState, payload?: number) => void
}

export const mutations: MutationTree<ExampleState> & ExampleMutations = {
    [ExampleMutation.INCREMENT]: (state, payload) => {
        if (payload === undefined) {
            throw new Error('Missing Payload')
        }

        state.count += payload
    },
}

Example/actions.ts

import { sleep } from '@/common/utils/sleep'
import { ActionContext, ActionTree } from 'vuex'
import { ExampleState } from '.'
import { ExampleGetters } from './getters'
import { ExampleMutations, ExampleMutation } from './mutations'

// ----------------------------------------------------------------------------
// Interfaces
// ----------------------------------------------------------------------------

export enum ExampleAction {
    INIT = 'INIT',
}

// ----------------------------------------------------------------------------
// Actions
// ----------------------------------------------------------------------------

type TypedActionContext = Omit<ActionContext<ExampleState, ExampleState>, 'commit' | 'dispatch' | 'getters' | 'rootState' | 'rootGetters'> & {
    commit<K extends keyof ExampleMutations>(
        key: K,
        payload?: Parameters<ExampleMutations[K]>[1]
    ): ReturnType<ExampleMutations[K]>

    // eslint-disable-next-line no-use-before-define
    dispatch<K extends keyof ExampleActions>(
        key: K,
        // eslint-disable-next-line no-use-before-define
        payload?: Parameters<ExampleActions[K]>[1]
    // eslint-disable-next-line no-use-before-define
    ): ReturnType<ExampleActions[K]>

    getters: {
        [K in keyof ExampleGetters]: ReturnType<ExampleGetters[K]>
    }
}

export interface ExampleActions {
    [ExampleAction.INIT]: (context: TypedActionContext) => Promise<void>
}

export const actions: ActionTree<ExampleState, ExampleState> & ExampleActions = {
    [ExampleAction.INIT]: async({ commit }) => {
        await sleep(1000) // Simulate calling API
        commit(ExampleMutation.INCREMENT, 42)
    },
}

Example/getters.ts

import { GetterTree } from 'vuex'
import { ExampleState } from '.'

// ----------------------------------------------------------------------------
// Interfaces
// ----------------------------------------------------------------------------

export enum ExampleGetter {
    DOUBLE = 'DOUBLE',
}

// ----------------------------------------------------------------------------
// Getters
// ----------------------------------------------------------------------------

export interface ExampleGetters {
    [ExampleGetter.DOUBLE]: (state: ExampleState) => number
}

export const getters: GetterTree<ExampleState, ExampleState> & ExampleGetters = {
    [ExampleGetter.DOUBLE]: (state: ExampleState) => {
        return state.count * 2
    },
}

Motivation

I've been using Vue 3 and Typescript for my personal side projects for awhile now. However, one of Vue's biggest weakness today is still its lack of comprehensive Typescript support. For example, its state management library, Vuex 4, still requires using strings as function names and any as payloads:

import { createStore } from 'vuex'

interface State {
    count: number
}

const store = createStore<State>({
    state: () => {
        return { count: 0 }
    },
    mutations: {
        increment: (state, amount: number) => {
            state.count += amount
        },
    },
})

// All of these are valid Typescript
store.commit('increment', 42)
store.commit('increment', { anything: 'can be here' })
store.commit('increment')
store.commit('inc', 42)

Fortunately I've stumbled upon this solution. Due to how verbose it is, I've built this tool to automate the repetitive boilerplate code for my future projects. Hopefully this tool will no longer be necessary by the time Vuex 5 is released.