Vuex with TypeScript: Tricks to improve your developer experience
Vue.js is a popular JavaScript framework which recently surpassed React in stars on GitHub. And TypeScript is getting more popular as well. So, combining the two is a likely choice for a new web app project. Our team made the same decision but soon found out that the Vue ecosystem and TypeScript are not a perfect match.
Vue itself has a decent TypeScript support nowadays and will even be re-written in TypeScript for its next major version. However, its official state management library Vuex still lacks thorough type definitions.
The type definitions for defining getters, mutations and actions are quite decent and the related code usually resides within one store or module file, so weak or missing type definitions are a minor issue here. It is the caller side – when you access the store from one of your Vue components – which we will focus on improving. The calling code is spread througout the whole code base, so this is where we benefit most from better type support.
The whole source code used in the following examples is available on GitHub.
There is one more thing we should clarify before we go into the details. We use Vue Class Component for defining our components because it is a really good fit with TypeScript classes. If you are using the standard Vue components definitions, the type definitions you need to change are probably quite different to what we are going to cover here.
State
Accessing the global state from any Vue instance is quite easy: this.$store.state
.
But when we take a look at the type of this.$store.state
, the IDE gives us a disappointing any
type.
The TypeScript compiler does not know which type our state is, even though we nicely defined our state interface when creating the store.
A look at how this.$store
is defined, gives us the reason for this.
|
|
What we need is $store
to be of type Store<State>
to get the full TypeScript support for this.$store.state
.
Unfortunately, we cannot override the type of this.$store
.
The Vuex type definitions use TypeScript’s Interface Merging feature to extend the default Vue interface and it does not allow changing the type of an existing property.
But we can redefine it with a different name:
|
|
The first part of the code defines a new property called $vStore
which can be accessed in any Vue instance.
The second part defines a Vue plugin which registers a getter $vStore
which simply returns $store
.
The last lines register both Vuex and our own plugin with Vue.
Now, we have beautiful code completion and proper refactoring support in our Vue components:
Getters
Getters are a bit trickier.
They are already defined as any
in Vuex’s type definition for Store
.
So we need to redefine it.
We could use interface merging again to redefine it with a different name but there is an easier way.
TypeScript allows us to override the property’s type to a more strict type in subclasses.
And with a bit more TypeScript wizardry, we can couple our own Getter
interface with the getter implementation definitions.
|
|
The GetterDefinition
interface ensures that we have all properties from the Getter
interface defined in our store ([P in keyof Getters]
) and that those defintions return the correct types (⇒ Getters[P]
).
Mutations and actions
Mutations and actions follow the same principle, so we will tackle them in this part collectively.
Mutations and actions are not defined as named properties like states and getters.
Instead, you call commit
/dispatch
and specify which mutations/action to use based on the passed string.
When calling commit
/dispatch
TypeScript is unable to check if a mutation or action with the passed name exists.
So instead we want a list of available mutations/actions and the easiest way to do that is specifying them using enums.
We can then override the commit
and dispatch
definitions in our MyStore
interface to only allow those enum values as types.
|
|
Similar to our GetterDefinition
from before, we use our custom ActionsDefinition
to ensure our store implementation matches the enum values.
The example above only shows the changes needed for actions but changing mutations would work in the same way.
Going further
Now we have type safety for which mutations and actions are allowed.
However, we are not fully done yet.
The payload for mutations and actions is still of type any
.
We can fix this by adding a small facade which ensures the passed payload has the right type.
|
|
We also changed our ActionsDefinition
to narrow the type of the payload correctly.
Unfortunately, this results in a significant change in how we are used to work with Vuex. If you are not comfortable going that far, feel free to omit this step and stay with the enum version from before.
Modules
We won’t cover support for modules in detail. However, we will briefly discuss how you could tackle it.
Namespacing Vuex modules is tricky. You need to add the namespace to every getter/mutation/action call outside the module which could prove difficult to typecheck properly. Instead you could try to avoid Vuex’s namespacing mechanism and enforce on your own that you don’t have any name conflicts between getters/mutations/actions of different modules. One possibility would be to define all possible getters/mutations/actions in one file/enum. Alternatively, you could create your own namespacing mechanism by prefixing the value of the TypeScript enum with the namespace, e.g.
|
|
Conclusion
Vuex was not built with TypeScript in mind and adding bullet-proof type checks is not an easy endeavor. In the end, adding those safety nets is a trade-off between having a more safe and comfortable developer experience and investing time and effort into something which solves a potentially negligible problem.