Properly typed Vuex Stores
TLDR: In this article, we discuss having fully type-checked Vuex stores. Full code is available in vinicius0026/properly-typed-vuex-stores.
This is the first article in our Structuring Large Vue.js Applications series. Here is the full list of released and planned articles:
- Properly typed Vuex Stores — published May 13, 2020 — You are here
- Adopting TypeScript in your Vue.js Application in a sane way — published May 14, 2020
- Modularizing the logic of your Vue.js Application — published May 15, 2020
- Data-driven components — published May 25, 2020
- Handling data at the edge of your Vue.js application — published June 1st, 2020
Vue.js and TypeScript are two trending topics in front-end development.
Vue.js calls itself “The Progressive JavaScript Framework”, meaning by that that the entry barrier to start using it is very low, but as one’s needs grow, Vue’s capabilities and its ecosystem will be there to match those needs.
One of the common patterns mid-sized to large applications need is centralized state management. Whenever your Vue app needs that, the no-brainer choice is Vuex, Vue’s own implementation of “State Management Pattern”.
When apps start to grow beyond mid-size, TypeScript starts to be more and more valuable to manage complexity.
This article discusses ways of putting all this together, to have a solid base for a large application using Vue, Vuex, and TypeScript.
Default Setup
Thanks to the fantastic vue-cli
tool, it is straightforward to start a Vue + Vuex + TypeScript project.
After installing vue-cli
, just run vue create <project name>
, answer a few questions and you will be up and running. Make sure to select Manually select features in the Please pick a preset stage and check TypeScript
and Vuex
.
When vue-cli
finishes scaffolding the app and installing all dependencies, we will have a Vuex store stubbed out for us, in TypeScript:
For large-sized app, we should break our store into modules to make it more manageable. Let’s create an auth
module, where we will keep track of the authenticated user in our app.
# pwd is the root fo the generated app
$ mkdir src/store/modules
$ touch src/store/modules/auth.ts
And then let’s create the Auth Store:
Here we are defining simplistic User
interface, with just a name; an AuthState
interface to use as the type for our store's state; and a getter and a mutation to get and set the user.
We can now register this module in the root store:
With that wired up, we can use the auth
store in our app. Let's add a login link that will eventually call our auth
store to set the authUser
to a fake user.
Here we have a couple of options when it comes to accessing our store from a component. We can either use the $store
accessor in the Vue component object or use Vuex helpers such as mapGetters
and mapMutations
. Let's try both.
Using this.$store
is pretty straightforward:
Using the mapMutations
helper is just a tiny bit more involved:
Both work fine, as we can check by using vue-devtools
:
But neither is a great experience with regards to type checking. Although we have added types to all the parameters in our store, we don’t get type-checking for the store mutation.
And in fact, they can lead to a bad state to be committed to the store:
But there is a Better Way™.
A Better Way™
To have our store functions properly type-checked, we will use vuex-typex
, a nice but oddly still little used library.
$ npm install -S vuex-typex
We will have to write our store in a slightly different way, but it will pay off by enabling type-checking across our whole app.
First of all, we will have to create a new file, where we will declare our RootState interface, and we will create our storeBuilder
object from vuex-typex
, based on our RootState
.
Now, let’s rewrite our auth
module to use vuex-typex
:
There’s a lot going on here. First, we have changed our AuthState
declaration from a typescript interface
to a class
. Then, we are using this class to instantiate a module builder (that we call just b
for ease of access). Notice how we are passing the AuthState
as a type parameter to the storeBuilder.module
call, and also using it to instantiate our initial state, by calling new AuthState()
.
Using the module builder, we can now declare our getter and our mutation. We use b.read
to declare getters and b.commit
to call mutations. Instead of exporting a default object with the state, getters, mutations, and actions, we individually export the state, getters, and mutations.
Now we need to change the store’s index file to use vuex-typex
:
It got actually simpler than before. One caveat here is that we have to make sure the modules are evaluated before we import the storeBuilder
from the RootState
file. We achieve that by an anonymous import (line 4 in the code above).
We now have to update the component’s code. Instead of using this.$store
or mapMutations
, we are going to import the auth
store module directly.
And now we have type-checking in our store!
And the type-checker will warn us of any mismatch on the types.
Getters
Getters are also type-checked as we can confirm by accessing the authUser
:
Notice the use of the optional chaining operator ?
. If we remove it, we will have a type error, because the authUser
is possibly null
.
Actions
Declaring actions in our type-checked store requires just a bit more effort than getters and mutations. We need to create a new type for the action context, like so:
Here we declare a login
action that, like any other Vuex action, takes the context
as the first argument. In this case, we use a special type for the context to be able to access, if needed, the current module state or global state inside the action.
The second parameter to the login
action is whatever we need to perform this action. Here we are using an object to pass in a username
and a password
, but it can be anything.
We defined a fake user service to represent the async call that would eventually return the user if the login is successful, and then we are calling the setUser
mutation.
To wire this action up to the store, we use the module builder dispatch
method (b.dispatch
).
We can now update our component to use this action to log the user in:
We now have a fully type-checked store!
Conclusion
vue-cli
provides an effortless way to get up and running with a standard Vue application layout. But, unfortunately, Vuex doesn't have full support for TypeScript out of the box.
We are getting close to Vue 3 launch, and it will have native support for TypeScript. Vuex 4.0 (that will be the vuex version compatible with Vue 3) is still in Beta at the time of this writing, and it still doesn’t provide full type-checking for Vuex stores declared with TypeScript. We hope to see improvements in type-checking as it gets closer to be released.
Anyway, for all the Vue 2.* applications out there, this article should help to get TypeScript’s full power to Vuex Stores.
Originally published at https://viniciusteixeira.tk on May 13, 2020.