Use a Vuex Store with Typing in TypeScript: a Solution Compatible with the Vue 3 Compositio...

I found a way to use Vuex stores in TypeScript without losing typing. It doesn’t require classes, so it is compatible with Vue 3 and the composition API.

Use lightweight wrappers that are fully typed

At runtime, the library direct-vuex will create a wrapper around each getters, mutations and actions. We can use them from outside the vuex implementation.

So instead of writing:

store.dispatch("myAction", myPayload);

… we write:


Or, instead of writing:

store.dispatch("myModule/myAction", myPayload);

… we write:


Getters and mutations are accessible the same way:


How to create a direct store

The store is implemented in the same way as usual. Here is a module:

// module1.ts
export default {
namespaced: true,
state: {
name: null as null | string
getters: {
message: state => `Hello, ${}!`
mutations: {
SET_NAME(state, newName: string) { = newName
actions: {
async loadName({ commit }, payload: { id: string }) {
const name = `Name-${}` // load from somewhere
commit("SET_NAME", name)
return { name }
} as const

Two minor details:

as const

Now, we can create the store using direct-vuex.

First, add direct-vuex to the Vue application:

npm install direct-vuex

Then, create the store:

// store.ts
import Vue from "vue"
import Vuex from "vuex"
import { createDirectStore } from "direct-vuex"
import module1 from "./module1"
Vue.use(Vuex)export default createDirectStore({
modules: {
} as const)

Compared to your usual code, new Vuex.Store(options) is replaced by createDirectStore(options) . In fact, the createDirectStore() function simply calls new Vuex.Store() for you, then returns a direct store with wrappers and a good typing around the classic Vuex store.

That’s it!

The store provided by direct-vuex already contains all the well typed state, getters, mutations and actions.

The classic Vuex store is still accessible through the store.original property. We need it to initialize the Vue application:

import Vue from "vue"
import store from "./store"
import App from "./App.vue"
new Vue({
store: store.original, // Inject the classic Vuex store
render: h => h(App),

Thus, the injected store is still the classic Vuex store. I suggest to ignore it and import directly our well typed store in each component that needs it:

import store from "./store";async function test() {
store.state.otherModule // Error. // Ok, type is 'string | null'.
store.state.module1.otherProp // Error.
store.getters.module1.message // Ok, type is 'string'.
store.getters.module1.otherProp // Error.
store.commit.module1.SET_NAME("abc") // Ok.
store.commit.module1.SET_NAME(123) // Error.
store.commit.module1.OTHER_PROP(123) // Error.
const result = await store.dispatch.module1.loadName({ id: "12" })
// Ok, result type is: '{ name: string }'
store.dispatch.module1.loadName({ otherPayload: "12" }) // Error

As you can see, there is no more any . The whole store is correctly typed, with the benefit of auto-completion in your IDE.

Note that direct-vuex is not a rewrite of Vuex but just a very lightweight wrapper. So, everything you know about Vuex still applies, because it’s still Vuex working underground. And you can simultaneously use the underlying Vuex store if you wish, through the injected this.$store or store.original .

I hope this small library can help.