Preface
🚨 The approach, described in this article, is not encouraged to be used in production, till Vuex
4.x
and Vue.js3.x
are completely released. Vuex4.x
and Vue.js3.x
API are still unstable. The article just illustrates my attempts to statically type Vuex store, sinceVuex@v4.0.0-beta.1
has removed its global types.
⚠️ The project configuration section is intentionally omitted. All the source code is located in this repository.
Introduction
Vuex@v4.0.0-beta.1
is officially released. One of the breaking changes that was introduced is that the library is no more shipped with global typings for this.$store
within Vue Component.
Vuex 4 removes its global typings for
this.$store
within Vue Component
More information about reasons and motivations behind it you can find in this issue.
Since global typings is removed, it's up to a developer to define it by himself. As stated in release notes:
When using TypeScript, you must provide your own augment declaration.
In this article I want to share my experience of augmenting types of a store. I will demonstrate this with an example of simple store. For simplicity, our store is as dumb as possible.
Let's do some coding.
State
A definition of a store starts with a definition of state.
state.ts
:
export const state = {
counter: 0,
}
export type State = typeof state
We need to export type of a state because it will be used in definitions of getters, mutations and actions.
So far so good. Let's go ahead to mutations.
Mutations
As stated in the Vuex docs:
It is a commonly seen pattern to use constants for mutation types in various Flux implementations.
So, all of our possible names of mutations will be stored in the MutationTypes
enum.
mutation-types.ts
:
export enum MutationTypes {
SET_COUNTER = 'SET_COUNTER',
}
Now that we have defined the names of mutations, we can declare a contract for each mutation (its actual type). Mutation is just a simple function, which accepts state as the first argument and payload as the second, and eventually mutates the former. State
type comes in action, it is used as the type of the first argument. The second argument is specific to a particular mutation. We already know that we have SET_COUNTER
mutation, so let's declare types for it.
mutations.ts
:
import { MutationTypes } from './mutation-types'
import { State } from './state'
export type Mutations<S = State> = {
[MutationTypes.SET_COUNTER](state: S, payload: number): void
}
Great! It's time to implement it.
import { MutationTree } from 'vuex'
import { MutationTypes } from './mutation-types'
import { State } from './state'
export type Mutations<S = State> = {
[MutationTypes.SET_COUNTER](state: S, payload: number): void
}
export const mutations: MutationTree<State> & Mutations = {
[MutationTypes.SET_COUNTER](state, payload: number) {
state.counter = payload
},
}
The mutations
variable is responsible for storing all of implemented mutations, and eventually will be used to construct the store.
MutationTree<State> & Mutations
intersection of types guarantees that a contract is correctly implemented. If it isn't, TypeScript complains and we get the following error:
Type '{ SET_COUNTER(state: { counter: number; }, payload: number): void; }' is not assignable to type 'MutationTree<{ counter: number; }> & Mutations<{ counter: number; }>'.
Property '[MutationTypes.RESET_COUNTER]' is missing in type '{ SET_COUNTER(state: { counter: number; }, payload: number): void; }' but required in type 'Mutations<{ counter: number; }>'
Just a few words about MutationTree
type. MutationTree
is a generic type, that is shipped with the vuex
package. From its name it's clear, that it helps to declare a type of mutation tree.
vuex/types/index.d.ts
:
export interface MutationTree<S> {
[key: string]: Mutation<S>;
}
But it's not specific enough to suit our needs, because it supposes that a name of mutation can be any string
, but in our case we know that a name of mutation can be only typeof MutationTypes
. We have left this type just for compatibility with Store
options.
Actions
There is no need for actions for such a simple store, but to illustrate typing for actions, let's imagine that we can fetch counter from somewhere.
In the same way as we store names of mutations we store names of actions.
action-types.ts
:
export enum ActionTypes {
GET_COUTNER = 'GET_COUTNER',
}
actions.ts
:
import { ActionTypes } from './action-types'
export const actions = {
[ActionTypes.GET_COUTNER]({ commit }) {
return new Promise((resolve) => {
setTimeout(() => {
const data = 256
commit(MutationTypes.SET_COUNTER, data)
resolve(data)
}, 500)
})
},
}
We have a simple GET_COUNTER
action which returns Promise
, which is resolved in 500ms. It commits the previously defined mutation (SET_COUNTER
). Everything seems okay, but commit
allows committing any mutation, which is inappropriate, because we know the we can commit just defined mutations. Let's fix it.
import { ActionTree, ActionContext } from 'vuex'
import { State } from './state'
import { Mutations } from './mutations'
import { ActionTypes } from './action-types'
import { MutationTypes } from './mutation-types'
type AugmentedActionContext = {
commit<K extends keyof Mutations>(
key: K,
payload: Parameters<Mutations[K]>[1]
): ReturnType<Mutations[K]>
} & Omit<ActionContext<State, State>, 'commit'>
export interface Actions {
[ActionTypes.GET_COUTNER](
{ commit }: AugmentedActionContext,
payload: number
): Promise<number>
}
export const actions: ActionTree<State, State> & Actions = {
[ActionTypes.GET_COUTNER]({ commit }) {
return new Promise((resolve) => {
setTimeout(() => {
const data = 256
commit(MutationTypes.SET_COUNTER, data)
resolve(data)
}, 500)
})
},
}
In the same way as we declare a contract for mutations we declare a contract for actions (Actions
). We must also augment the ActionContext
type which is shipped with the vuex
package, because it supposes we can commit any mutation. AugmentedActionContext
do the job, is restricts committing only the declared mutations (it also checks payload type).
Typed commit
inside actions:
Improperly implemented action:
Getters
Getters are also amenable to be statically typed. A getter is just like mutation, and is essentially a function which receives state as its first argument. A declaration of getters is not much different from a declaration of mutations.
getters.ts
:
import { GetterTree } from 'vuex'
import { State } from './state'
export type Getters = {
doubledCounter(state: State): number
}
export const getters: GetterTree<State, State> & Getters = {
doubledCounter: (state) => {
return state.counter * 2
},
}
Global $store
type
Core modules of the store have been defined, and now we can actually construct the store. A processes of store creation in Vuex@v4.0.0-beta.1
is slightly different from Vuex@3.x
. More information about it is located in release notes. The Store
type should be declared to safely access the defined store in components. Note that default Vuex types: getters
, commit
and dispatch
should be replaced with types which we have defined earlier. The reason of this replacement is that default Vuex store types is too general. Just look at the default getters types:
export declare class Store<S> {
// ...
readonly getters: any;
// ...
}
Without a doubt, these types are not suitable in case you want to safely work with a typed store.
store.ts
:
import {
createStore,
Store as VuexStore,
CommitOptions,
DispatchOptions,
} from 'vuex'
import { State, state } from './state'
import { Getters, getters } from './getters'
import { Mutations, mutations } from './mutations'
import { Actions, actions } from './actions'
export const store = createStore({
state,
getters,
mutations,
actions,
})
export type Store = Omit<
VuexStore<State>,
'getters' | 'commit' | 'dispatch'
> & {
commit<K extends keyof Mutations, P extends Parameters<Mutations[K]>[1]>(
key: K,
payload: P,
options?: CommitOptions
): ReturnType<Mutations[K]>
} & {
dispatch<K extends keyof Actions>(
key: K,
payload: Parameters<Actions[K]>[1],
options?: DispatchOptions
): ReturnType<Actions[K]>
} & {
getters: {
[K in keyof Getters]: ReturnType<Getters[K]>
}
}
I will not focus on the TypeScript's Utility Types.
We are at the finish line. All is left is the augmentation of the global Vue types.
types/index.d.ts
:
import { Store } from '../store'
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$store: Store
}
}
Perfect! We are ready to enjoy a fully typed store access.
Usage in components
Now that our store is correctly declared and is statically typed, we can utilize it in our components. We will take a look at a store usage in components defined with Options API and Composition API syntax, since Vue.js 3.0 supports both.
Options API
<template>
<section>
<h2>Options API Component</h2>
<p>Counter: {{ counter }}, doubled counter: {{ counter }}</p>
<input v-model.number="counter" type="text" />
<button type="button" @click="resetCounter">Reset counter</button>
</section>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { MutationTypes } from '../store/mutation-types'
import { ActionTypes } from '../store/action-types'
export default defineComponent({
name: 'OptionsAPIComponent',
computed: {
counter: {
get() {
return this.$store.state.counter
},
set(value: number) {
this.$store.commit(MutationTypes.SET_COUNTER, value)
},
},
doubledCounter() {
return this.$store.getters.doubledCounter
}
},
methods: {
resetCounter() {
this.$store.commit(MutationTypes.SET_COUNTER, 0)
},
async getCounter() {
const result = await this.$store.dispatch(ActionTypes.GET_COUTNER, 256)
},
},
})
</script>
Composition API
To use store in a component defined using Composition API, we must access it via useStore
hook, which just returns our store:
export function useStore() {
return store as Store
}
<script lang="ts">
import { defineComponent, computed, h } from 'vue'
import { useStore } from '../store'
import { MutationTypes } from '../store/mutation-types'
import { ActionTypes } from '../store/action-types'
export default defineComponent({
name: 'CompositionAPIComponent',
setup(props, context) {
const store = useStore()
const counter = computed(() => store.state.counter)
const doubledCounter = computed(() => store.getters.doubledCounter)
function resetCounter() {
store.commit(MutationTypes.SET_COUNTER, 0)
}
async function getCounter() {
const result = await store.dispatch(ActionTypes.GET_COUTNER, 256)
}
return () =>
h('section', undefined, [
h('h2', undefined, 'Composition API Component'),
h('p', undefined, counter.value.toString()),
h('button', { type: 'button', onClick: resetCounter }, 'Reset coutner'),
])
},
})
</script>
Conclusion
The result of our efforts is fully statically typed store. We are allowed to commit/dispatch only declared mutations/actions with appropriate payloads, otherwise we get an error.
By now Vuex does not provide correct helpers to facilitate process of typing, so we have to do it manually. Hope, that the following versions of Vuex will be shipped with the flexible store typing.
Top comments (40)
Wow! I totally didn't expect that this article will be popular. Thank you for your comments.
According to the comments I've read, here is what I'm planning to include:
namespaced
modules;What do you think of idea of creating a dedicated package with helper types (
MutationTree
,GetterTree
,ActionTree
,Store
)?I also managed to type
getters
,rootState
,rootGetters
inactions
andgetters
.Stay tuned.
Hey Andrew, thanks for this great example. I am currently stuck in the process of typing getters, rootState, and rootGetters in actions and getters. Any chance that you can share an example? I guess the AugmentedActionContext and Store type definitions is what I am looking for.
Thank You
As a reference, I've uploaded a gist here: gist.github.com/soerenmartius/5f69...
The code works but typescript is unable to resolve the getters properly. Any help will be highly appreciated
Thank you very march for great article!
You promice to add some new code as you described above.
May be you have any news about it ?
Hi @3vilarthas
Did you share the update avoid the Types for rootState? I would like to see this resource. Thanks in advance!
Hey Andrew, any thoughts on the namespaced modules yet? Great guide otherwise!
Hey Sergej, Thanks. You have to manually create
MODULE_NAME/MUTATION_TYPE
mappings by now. But if you are using TS4.1
you can use Template Literal Types.Here is the snippet from the helper library I am developing right now:
I see, nice trick, thanks, Andrew!
Hey Andrew, thanks for this great example.
can you please help with namespaced modules in vuex4 with typescript
I'm using your suggested way but I'm still getting the error
type Namespaced = {
[P in keyof T & string as
${N}/${P}
]: T[P]}
type NamespacedMutations = Namespaced
Hi, Harsh. I tried your method, but I still cannot use
store.dispatch('moduleName/actionName'), is there any fixes..
Yes I solved, This is the solution you can refer
If you want a complete boilerplate with vue3 and other more configs you can clone or fork my repository.
github.com/techarshgupta/vue3-boil...
Thank you!
I tried this method but seems like it will disable type check when I use
store.dispatch('moduleName/actionName') because I can pass any string and the compiler can compile successfully
type Namespaced = {
};
type NamespacedActions = Namespaced;
no, it will give an error when you compile you can check my repo that I mentioned.
I am still disappointed with how difficult Vuex+Typescript is to set up. I will keep using module-decorators (github.com/championswimmer/vuex-mo...) which let you have complete type safety and no redundancy in your code:
And then just use it in a component with:
Thanks you so much for this amazing start-point!
I wanted to evaluate the viability of Vuex + Typescript when compared to my known Redux + Typescript, as I'm not too happy with Redux. But as I have never used Vue nor Vuex in production before, bringing everything together is pretty challenging.
I'm quite decent in Typescript along though, and I noticed a few things regarding its usage where I would do things slightly different, so I thought I would write them here so you can either explain why you deem them necessary or maybe learn a tiny bit from a thankful reader:
I noticed a number of duplic typing. I.e. in src/store/mutations.ts you define the SET_COUNTER parameters typeonce in the static type (as you have to if you define the type statically), and once in the actual implementation. You could get rid of the type in the implementation, it would still be types, just as the state field is.
while we're there, why did you add the
MutationTree<State>
annotation? If you happend to define a mutation with the wrong type you would at the latest get warned in thecreateStore
function, I would think at least. Additionally if you get rid of that annotation you could also ease typing by implicitly inferring the type with typeof.Honestly, I never use the Enum type in Typescript again since I found the Union of string literals (
"OPTION_1" | "OPTION_2"
). Overall the purpose of these Enums in Vuex (and i.e. Redux too) is a moot point IF you have typed everything correctly all the way through. I personally do not declare enums for the different Actions/Mutations, just identifying by the function name is more than sufficient.I would prefer to declare the type of Store in the
.d.ts
fileThank you! It's vary nice article! However, how to apply it in
mapState, mapGetters, mapMutations, mapActions
?Great question
So, I was applying all the wisdom in here, to my quite large vuex stack, and I stumbled upon the second argument of Getters (I leave the third and fourth to the reader)
Basically, the second argument to Getters references the Getters for the current namespace, but not as functions, as values directly.
For example, I have this:
The problem is that the getters argument as an "any" type everywhere, I tried to put the
Getters
type, but that was not right. After a bit of fiddeling around (I am still a bit new and rough in typescript) I ended up having this :The naming is probably awful, and now that I think about it, one should just have to generate a
Getters
that is, say,user: PayloadUser; roles: string[];
and a different kind of fiddeling be done so that the type with the function definitions can be generated automatically.Also, if all this goes into a module somewhere, this should probably get there too.
So, I thought about what I wrote in the end and changed my code, it now reads:
Not sure about it, but I think you have a typo:
Shouldn't this be
store.ts
orindex.ts
?Anyways. Thank you for this short explanation on how to type the new vuex store 🦄🎉🥳
Thanks! Fixed.
Thank you for this, is a great post and got me started, thanks!
For anybody who is implementing this with Modules, I'm not a Typescript expert at all, but i got it working, I'm not 100% sure I did it in the best way but it is working as expected for me. You can find an example of my implementation here:
gist.github.com/javisperez/b13d020...
Open to suggestions or questions, I hope this helps somebody.
I'm going to include a section about
namespaced
modules soon.actually, I have a question, according to this line
payload: Parameters[1]
it forces me to commit a mutation with payload, but what if I just want to commit a mutation without a payload?
like the RESET_COUNTER action in your article
You can mark the
payload
parameter as optional.I have same problem. Did you have the solution?
I see you are using type intersection for example
GetterTree<State, State> & Getters
. I have a general dbt, why did you choose this way but not interfaces.In interfaced you in extend the
GetterTree<State, State>
and then use it in the Getters part.Some comments may only be visible to logged-in visitors. Sign in to view all comments.