Use a Vuex Store with Typing in TypeScript: A Solution Compatible with the Vue 3 Composition API

Paleo
ITNEXT
Published in
3 min readOct 13, 2019

--

Thanks to Good Free Photos.

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.

With 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 store implementation.

So instead of writing:

store.dispatch("myAction", myPayload);

… we write:

store.dispatch.myAction(myPayload);

Or, instead of writing:

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

… we write:

store.dispatch.myModule.myAction(myPayload);

Getters and mutations are accessible the same way:

store.getters.myModule.myGetter;
store.commit.myModule.myMutation(myPayload);

The types of these wrappers are correctly inferred by TypeScript.

How to create a direct store

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

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

A minor detail anyway: When we write the code of a store the old way, the only little thing is that it is necessary to declare namespaced: true as true. Direct-vuex needs to know the literal type of namespaced (the type true or false instead of boolean).

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)const { store, rootActionContext, moduleActionContext } = createDirectStore({
modules: {
module1
}
})
// Export the direct-store instead of the classic Vuex store.
export default store
// The following exports will be used to enable types in the
// implementation of actions.
export { rootActionContext, moduleActionContext }
// The following lines enable types in the injected store '$store'.
export type AppStore = typeof store
declare module "vuex" {
interface Store<S> {
direct: AppStore
}
}

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.

Then, we augment the type Store from the module vuex with our well-typed direct store.

That’s it!

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

Use a direct store

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),
}).$mount("#app")

The injected store is still the classic Vuex store. But it has a direct property that contains our well-typed store. We can use it or directly import the store in each component that needs it:

import store from "./store";
// or: const store = this.$store.direct;
async function test() {
store.state.otherModule // Error.
store.state.module1.name // 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.

Write the implementation of a store using Direct-vuex

Here is the documentation.

I hope this small library can help.

--

--