- Vuex + TypeScript
- Introduction
- State
- Mutations
- Actions
- Getters
- Global $store type
- Usage in components
- Options API
- Composition API
- Conclusion
- Building Web Apps with Vue 3 composition API + Typescript + Vuex(4.0)
- Pre-requisite
- Introducing Vue composition API
- Introducing Vuex(4.0)
- Setting up vue 3 + vuex + TypeScript App
Vuex + TypeScript
π¨ The approach, described in this article, is not encouraged to be used in production, till Vuex 4.x and Vue.js 3.x are completely released. Vuex 4.x and Vue.js 3.x API are still unstable. The article just illustrates my attempts to statically type Vuex store, since Vuex@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.
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:
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
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
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 MutationsS = State> = [MutationTypes.SET_COUNTER](state: S, payload: number): void >
import MutationTree > from 'vuex' import MutationTypes > from './mutation-types' import State > from './state' export type MutationsS = State> = [MutationTypes.SET_COUNTER](state: S, payload: number): void > export const mutations: MutationTreeState> & 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
Type '< SET_COUNTER(state: < counter: number; >, payload: number): void; >' is not assignable to type 'MutationTree> & Mutations>'. Property '[MutationTypes.RESET_COUNTER]' is missing in type '< SET_COUNTER(state: < counter: number; >, payload: number): void; >' but required in type 'Mutations>'
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 MutationTreeS> [key: string]: MutationS>; >
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', >
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 = commitK extends keyof Mutations>( key: K, payload: ParametersMutations[K]>[1] ): ReturnTypeMutations[K]> > & OmitActionContextState, State>, 'commit'> export interface Actions [ActionTypes.GET_COUTNER]( commit >: AugmentedActionContext, payload: number ): Promisenumber> > export const actions: ActionTreeState, 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: GetterTreeState, 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 StoreS> // . 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 VuexStoreState>, 'getters' | 'commit' | 'dispatch' > & commitK extends keyof Mutations, P extends ParametersMutations[K]>[1]>( key: K, payload: P, options?: CommitOptions ): ReturnTypeMutations[K]> > & dispatchK extends keyof Actions>( key: K, payload: ParametersActions[K]>[1], options?: DispatchOptions ): ReturnTypeActions[K]> > & getters: [K in keyof Getters]: ReturnTypeGetters[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 > >
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> Options API Component Counter: <counter >>, doubled counter: <counter >> v-model.number="counter" type="text" /> type="button" @click="resetCounter">Reset counter 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>
Typed state :
Typed getters :
Typed commit :
Typed dispatch :
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>
Typed state :
Typed getters :
Typed commit :
Typed dispatch :
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.
Building Web Apps with Vue 3 composition API + Typescript + Vuex(4.0)
In this tutorial, we are going to create a task management application to demonstrate how to build applications with the new Vue 3 composition API, typescript, and Vuex(4.0). Furthermore, we will explore Vuex(4.0) practically.
Pre-requisite
Node.js 10.x and above
Knowledge of JavaScript Es6
Basic knowledge of vue.js
Basic knowledge of Typescript
Introducing Vue composition API
The composition API is a new API for creating components in vue 3.
It presents a clean and flexible way to compose logic inside and between components. Also, it solves the problems associated with using mixins and higher-order component to share re-usable logic between components. It has a small bundle size and naturally supports typescript.
Introducing Vuex(4.0)
Vuex is a state management library created by the Vue team and built solely for use with Vue. It provides an intuitive development experience when integrated into an existing Vue app. Vuex becomes crucial when your Vue app gets more complex as it grows.
The latest version of Vuex, version v4.0.0 supports the new Composition API introduced in Vue 3 as well as a more robust inference for TypeScript.
In this article, we will explore the latest version of Vuex, version v4.0.0 practically as we build along.
Setting up vue 3 + vuex + TypeScript App
Letβs start by creating a Vue 3 app with typescript support using the Vue-CLI tool.
Take the following steps to create a Vue 3 + Vuex app with Typescript support, using Vue CLI tool