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.