Vuex 4 Typescript Declarations Generator

Update: Apr 28, 2022
Vuex is officially deprecated and should not be used anymore. You should use Pinia instead which already has great Typescript support out of the box. Alternatively if your state is simple, you can simply use a global reactive object.

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.