Vuex with TypeScript: Tricks to Improve your Developer Experience
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.
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 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.
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 (
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
dispatch and specify which mutations/action to use based on the passed string. When calling
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
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.
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.
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.
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.