Vuex + TypeScript
Tags: vue, typescript
Published: 2020-05-04 10:03:23

## Preface

> 🚨 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`](https://github.com/vuejs/vuex/releases/tag/v4.0.0-beta.1) has removed its global types.

<hr/>
  
> ⚠️ The project configuration section is intentionally omitted. All the source code is located in this [repository](https://github.com/andrewvasilchuk/vuex-typescript).

## Introduction

[`Vuex@v4.0.0-beta.1`](https://github.com/vuejs/vuex/releases/tag/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](https://github.com/vuejs/vuex/issues/994).

Since global typings is removed, it's up to a developer to define it by himself. As stated in [release notes](https://github.com/vuejs/vuex/releases/tag/v4.0.0-beta.1):

> 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`:

```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](https://vuex.vuejs.org/guide/mutations.html#using-constants-for-mutation-types):

> 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`:

```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`:

```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.

```ts
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:

![TypeScript complains about an unimplemented contract](https://dev-to-uploads.s3.amazonaws.com/i/72cpt8z9j55e6rocyak6.png)

```
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`:

```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`:

```ts
export enum ActionTypes {
  GET_COUTNER = 'GET_COUTNER',
}
```

`actions.ts`:

```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.

```ts
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:

![Typed commit](https://dev-to-uploads.s3.amazonaws.com/i/f67qnpzp6t2bg9pz0vx8.png)

Improperly implemented action:

![Improperly implemented action](https://dev-to-uploads.s3.amazonaws.com/i/2pmkl948r3dwfkxlwts8.png)

## 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`:

```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](https://github.com/vuejs/vuex/releases/tag/v4.0.0-alpha.1). 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:

```ts
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`:

```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](https://www.typescriptlang.org/docs/handbook/utility-types.html).

We are at the finish line. All is left is the augmentation of the global Vue types.

`types/index.d.ts`:

```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

```vue
<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>
```

Typed `state`:
![Typed state](https://dev-to-uploads.s3.amazonaws.com/i/vyiuligyga1nsopyqt60.png)

Typed `getters`:
![Typed getters](https://dev-to-uploads.s3.amazonaws.com/i/8ryrcie25hml3yggq1vv.png)

Typed `commit`:
![Typed commit](https://dev-to-uploads.s3.amazonaws.com/i/2g2qn3yydmupdzmap5ab.png)

Typed `dispatch`:
![Typed dispatch](https://dev-to-uploads.s3.amazonaws.com/i/tnrghaejk29s5lc2jck6.png)


### Composition API

To use store in a component defined using Composition API, we must access it via `useStore` hook, which just returns our store:

```ts
export function useStore() {
  return store as Store
}
```

```vue
<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 state](https://dev-to-uploads.s3.amazonaws.com/i/nlavgul8juzdcjxdf1op.png)

Typed `getters`:
![Typed getters](https://dev-to-uploads.s3.amazonaws.com/i/rou41bw3g0cqk5xbdo6h.png)

Typed `commit`:
![Typed commit](https://dev-to-uploads.s3.amazonaws.com/i/xpk2dp0aij7vtd88j9z6.png)

Typed `dispatch`:
![Typed dispatch](https://dev-to-uploads.s3.amazonaws.com/i/l5ettbkl084zoez0xrz5.png)

## 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.