The State of Typed Vuex: The Cleanest Approach
Are you avoiding using Vuex because there’s just too much boilerplate or because it doesn’t fully provide TypeScript support? Read on

If you’ve tried to use Vuex with TypeScript, you’ll have realized that it’s not such a straightforward task. Vuex doesn’t provide tooling for working with TypeScript out of the box. TypeScript is important for many of us because it allows us to write statically typed JavaScript, with the main benefit being improved development experience via IDE type hits.
What we want to accomplish is type-safe Vuex modules that inform us when we’re passing the wrong mutation/getter/action type or the wrong arguments — and this is possible in a couple of different ways. We will take a look at making this work with no external libraries, and then we’ll evaluate the options currently out there. Finally, we’ll make a rather subjective determination of which one is the cleanest.
These are the options we’ll be trying:
- Plain
- vuex-class
- vuex-typex
- vuex-smart-module
- vuex-class-component
- vuex-class-modules
- vuex-module-decorators
Even though Vue 3 is around the corner, along with the corresponding Vuex 4, usage won't vary from the Vuex side, as its changes are mostly related to Vue 3 compatibility. To clear up any confusion, Vue 3’s Reactivity API is not meant to replace Vuex. It just provides better tooling to handle those cases in which Vuex is already not necessary.
With that said, there was a talk at Vue.js Global Online Conference that outlines the roadmap for Vuex 5. It will have no mutations, just actions, and new ways to define/register stores, including regular composition API. With these changes, none of the outlined options here will be necessary for TypeScript integration. Unfortunately, it isn’t likely to ship until sometime in 2021, as there isn’t even an RFC for it yet.
Enough talking, let’s write some code. Feel free to go straight to the live example to see for yourself the different options at work in this sandbox, or take a look at this repo.
Setup
It’s important to note that Vuex separates functionality in modules. These modules are defined independently from one another and are meant for representing logical groups of information and corresponding functionality. Let’s define some common types:
Each module will have a persons
property that will be a mapping between the person’s id
and its Person
. We’ll have an action that’ll populate this property with static data and a getter that aggregates a persons’s name
and lastName.
This is what our stores
file will look like in Vue 2/Vuex 3:
Vue 3/Vuex 4:
We have two stores here because plainStore
will be typed entirely, passed into the Vue constructor, and accessed directly or through the $store
property in Vue component instances. But modulesStore
will not be registered, and its modules will be imported separately. The Vuex.Store
constructors/createStore
methods are being passed empty objects because we will register the modules dynamically.
Our main.ts
in Vue 2/Vuex 3:
Vue 3/Vuex 4:
I will later explain where the PlainStore
type comes from. For demonstration purposes, the Vue component definitions will be using vue-property-decorator, which depends on vue-class-component, although the usage would be the same with composition API.
1. Plain
You can indeed accomplish a fully typed Vuex store with no external libraries. We’ll need to define these types for the plain store:
PersonModule
: Will be the type of the plain
Vuex moduleRootState
: Will be the type of the root state (the state for plainStore
). We will stick the plain module into the plain
property of the root state.
Now we can write the mutations:
MutationTypes
: An enum
with the name of the mutations. We do this to avoid having to type mutation names manually across the application and get auto-completion functionality in the IDE.
Mutations
: The definition of mutations’ type signatures. This is needed for type-safe implementation of only these defined mutations and so that we can construct the more complex PlainStore
type later on.
mutations
: The mapping of mutation names to their implementation. At this point, we will be getting type-checking for the parameters state
and persons
, thanks to the union between Vuex’s built-in type MutationTree
and our Mutations
type.
The getters follow a similar approach:
Now actions, which have an added component:
When you define an action function in Vuex, the first parameter is of type ActionContext
which contains a commit
function. This function allows you to trigger mutations defined in the current module (or any other module, if {root: true}
is specified). The default type of this commit function lets you call commit
with any string. We want to restrict this so that commit
can only be called with the mutations defined in our module.
This is accomplished by augmenting the default ActionContext
and modifying the type of the commit
function accordingly. Keep in mind that if you want to call global mutations or mutations from other modules, specifying {root: true}
in commit
functions from within actions, you have to aggregate all of the mutations and use that to augment the ActionContext
instead of just the mutations from this module like we’re doing here. (I do this below to create the PlainStore
type.)
Now that we’ve successfully defined our Vuex module, we have to register it on the store:
The last thing we need to do is construct the PlainStore
type, which is the type of the actual store on which you access state
, commit
, dispatch
, and getters
. We want these properties to be type-checked and get errors if we try to access non-existent state
or getters
, or call commit
or dispatch
, with the wrong type
or parameter.
To define PlainStore
, we want to gather all of the mutations, getters, and actions from all of our modules and merge them through union with TypeScript’s &
operator.
We’re done defining and registering the Plain
person module. We have two options on how to access it in our components. We can access it through this.$store
(injected) or we can import the store directly in each component (imported).
Injected
To have access to our typed store through this.$store
, we’d need to override Vuex’s default $store
type definition, which is currently only supported in Vuex 4 (with Vue 3), so if that’s you, all you need to do is:
But if you’re using Vuex 3 (with Vue 2), you need to create another property, as you can’t override $store
typings. We can call it $vuex
(or anything else):
And then we need to define the actual property, which would just be a getter for $store
:
Then you could access either $store
or $vuex
(depending on what you did above) like so:
Imported
This usage is much more straight forward because we exported the store with its PlainStore
type, so we can just import it and use it directly:
And as you would expect,$store
(or whatever you called your store property) would be fully typed in both Injected and Imported scenarios.
Namespaced modules
How would this look if we wanted to namespace our Plain module? We would need to define separate enumerations and type signatures for each of our mutations, getters, and actions.
The main problem is that inside our module definition, all of the name constants don’t use the namespace prefix, because they are localized. Outside the module definition, they do have the prefix. So inside of an action of a namespaced module, you can commit nameOfMutation
, but if you want to commit it from a component, you’d have to commit prefix/nameOfMutation
.
A recently merged (dev branch) TypeScript feature would theoretically allow us to construct the namespaced signatures dynamically, by means of template literal types. Unfortunately, I couldn’t make this work, so we’re stuck with redefining the signatures one by one.
I will implement this for mutations only, so you get an idea:
We had to define separate enums for our different mutations with and without the namespace prefix (internal ones without the prefix to use in the definition of this module, and external ones with the prefix to export and use elsewhere).
Impressions
That was quite painful. It’s so explicit if it was a song it would be banned in 69 countries. This approach would get out of hand very quickly with large modules. Not a pleasant experience.
2. vuex-class
The first third-party solution we’ll look at is vuex-class. Although it is technically a TypeScript library, it doesn’t really provide any tooling to actually implement type safety. Therefore, it unfortunately still requires a significant effort to implement properly at scale. You don’t need a typed store definition to use it because vuex-class doesn’t access it anyway. The type would be directly in the component:
The problem with this is that you need to define the type of each resource manually. If you’re using the same resources in different components, you’ll have the same types defined in each of them. This is not ideal at scale because the types among components can differ, so changing the implementation or type of any of those resources and not successfully updating every component that’s accessing it can cause bugs that don’t become immediately obvious, bugs which TypeScript is designed to prevent.
In order to effectively implement vuex-class, it’s best that we define, the same way we did with the plain approach above, the store resources with their type signatures. If we do that, we could then construct a type that aggregates the relevant resources for easy access:
Which we could then use in our component:
But at this point, the juice is definitely not worth the squeeze.
Impressions
To use vuex-class effectively, significant manual labor is still required of us. Therefore, it provides no advantage over just using an imported store typed using the plain approach. vuex-class is not something I’d recommend any team using.
3. vuex-typex
The first library we’re going to look at to truly save us some keystrokes is vuex-typex. It has a composable API that prompts us to define everything separately and then bring it all together for the module definition.
The mutations and actions are defined like plain functions, but getters are defined through the module builder, either through b.read
or b.state
. Everything is put together into a constant that we later export. This API is rather confusing, though, because the thing from which we get the plain Vuex module that we register or the thing we associate the store to is not the module we just put together but the module builder. We’re registering the module using the Vuex store directly and then associating the module builder to the store.
This is how we’d use it in a component:
Impressions
The API is a bit weird, all over the place, and not very clean looking. The module builder API is just not very intuitive, and it seems we have to call a bunch of functions on it to register stuff properly.
4. vuex-smart-module
We’ll now explore a library made by one of Vuex’s core contributors, vuex-smart-module. It stays as close to the Vuex API as possible. This library namespaces modules by default. This is where things start getting interesting.
We can see that this is very close to how we would typically define our Vuex module. Instead of having an object for each type of resource, we would have a class with the resources as class functions/accessors.
Once we’re done defining them, we pass them into the provided Module
function, which assembles the resources. We’re registering the module with vuex-smart-module’s own registerModule
function. It takes as arguments the store, the name of the path of this module within the root state
, the path of the namespace, and finally, the module that we’re registering.
To export a ready-to-use Vuex module, the Module
instance has a context
method that returns the module associated with the passed-in store. We can import this module directly in our components:
Impressions
This is a solid solution. It builds on top of, and respects, the original Vuex API, leveraging the power of classes, containing both type signature and implementation. It was made by the author of Vuex, so we know it’s robust.
5. vuex-class-component
vuex-class-component uses class syntax as well to define the Vuex module, but it goes a step further by allowing us to define everything in one class. This class is then converted into a Vuex module object definition that can be used like any other. This library namespaces modules by default.
That’s a breath of fresh air. As you can see, we are provided with the mutation
and action
decorators, which are used to create the resources of the same name from functions. Class properties become state properties, and get
accessors become conventional class getters.
This time we’re using Vuex’s own registerModule
to dynamically register it into the store instance modulesStore
. createProxy
just returns a proxy to our module associated to the modulesStore
.
It can be imported and used directly:
The module is imported directly, type safety being ensured.
Impressions
This is what we’ve always wanted: a clean way to define our Vuex modules, and full type safety when accessing them. Can this be topped? We’ll have to continue and see.
6. vuex-class-modules
vuex-class-modules is a fork of vuex-class-component. What it improves is just the way the class is defined:
When we compare it to the module definition of vuex-class-component, we can see that we now have a Module
decorator and that we can use the same class we defined to create a new instance that will do two things: register the module in the store and return a usable, namespaced module (namespaced by default).
The component usage would be the exact same as vuex-class-component
.
Impressions
It’s not too different from vuex-class-component. We just get more tools to save us a couple of keystrokes. Pretty solid.
7. vuex-module-decorators
vuex-module-decorators implements the same concept as the previous libraries (class syntax). However, we get a few more useful decorators.
vuex-module-decorators provides a Module
decorator that wraps our class and accepts some options. The store
option will take care of associating this module to our modulesStore
, and the name
will be the property under which this module will live in the state, as well as its namespace (this library namespaces by default).
vuex-module-decorators provides the getModule
function, which will return a typed usable module that can be used the exact same way as vuex-class-component in our components.
It has long been a pain point of Vuex, the fact that we need to create mutations for every property we want to change in the state. Through the very interesting decorator MutationAction
, we don’t need to create a mutation if we just want to directly assign one or more state properties.
The properties contained in the object returned from the decorated function will be assigned directly to the corresponding state properties via a mutation that’s created under the hood. This is an impactful helper decorator that significantly improves the Vuex experience.
Impressions
This one provides the most concise and compact API for defining and using Vuex modules, leveraging classes, and providing additional decorators with options, to make development easier.
Cleanest
There’s no question about it. From all the options we discussed, vuex-module-decorators takes the cake. Its API is simple and gives us access to a feature (although emulated) that will not come until the release of Vuex 5: the merging of actions and mutations. It allows us to write the least code needed while not compromising on readability or usability.
Conclusion
When looking to integrate TypeScript into your store, if you don’t want to use any external libraries, the plain approach has got you covered (if you’re into that sort of thing; not sure why you would do that to yourself, though). If you want a better development experience, we have plenty of great options here.
I wouldn’t personally recommend plain, vuex-class, or vuex-typex, as they could get out of hand very rapidly. But vuex-smart-module, vuex-class-decorator, vuex-class-modules, and vuex-module-decorators are all great alternatives that receive decent community acceptance, the most popular by far being vuex-module-decorators (and the recipient of my cleanest title). They’re also very much plug-and-play, so you don’t have to rewrite all your store modules, you can just define and dynamically register new ones.
Again, you can see them live in this sandbox or take a look at the code.
Performance is a nonfactor for any of these libraries because in the end, they all build and register a regular old Vuex module.
So don’t fear Vuex. Working with it can be delightful and simple, and you can benefit from great dev tools support, with real-time inspection and time-travel debugging.
Now let’s go out there and start writing some clean, type-checked Vuex modules!