Integrate Vuex And TypeScript
Published: 2023-11-21 11:02:44

by [Esther Christopher](https://blog.openreplay.com/authors/esther-christopher)


<blockquote><em>
Vuex is a well-known state management library for Vue, and TypeScript adds data typing to your code to detect and avoid errors, so using both together is quite logical, and this article will show you how to do so.
</em></blockquote>


<div style="background-color:#efefef; border-radius:8px; padding:10px; display:block;">
<hr/>
<h3><em>Session Replay for Developers</em></h3> 
<p><em>Uncover frustrations, understand bugs and fix slowdowns like never before with <strong><a href="https://github.com/openreplay/openreplay" target="_blank">OpenReplay</a></strong> — an open-source session replay suite for developers. It can be <strong>self-hosted</strong> in minutes, giving you complete control over your customer data.</em></p>
<img alt="OpenReplay" style="margin-top:5px; margin-bottom:5px;" width="768" height="400" src="https://raw.githubusercontent.com/openreplay/openreplay/main/static/openreplay-git-hero.svg" class="astro-UXNKDZ4E" loading="lazy" decoding="async">
<p><em>Happy debugging! <a href="https://openreplay.com" target="_blank">Try using OpenReplay today.</a></em><p>
<hr/>
</div>


[Vuex](https://vuex.vuejs.org/) is the official state management library made for Vue.js. As an application expands and the number of components increases, handling shared state becomes increasingly challenging. In response to this complexity, Vuex was introduced. It offers a unified approach to managing and updating the state, ensuring consistent and traceable changes.

The creation of Vuex was influenced by the state management patterns and practices from other ecosystems, like Flux from the React community, but it was built specifically to integrate seamlessly with Vue.

[TypeScript](https://www.typescriptlang.org/) essentially offers a set of beneficial tools on top of JavaScript. It is a strongly typed superset of JavaScript developed by Microsoft. TypeScript introduces static typing to JavaScript, which means you can specify that a variable should only hold a certain primitive type such as string, boolean, number, etc. If you assign an unspecified type, the TypeScript compiler should throw an error. It also allows the definition of more complex types, such as interfaces and enums. 

There is also an important advantage of compile-time type checking, meaning more errors get caught during compile time rather than runtime, which also means fewer bugs in production. Most JavaScript libraries are also supported and compatible with TypeScript, including enhancing the capabilities of Integrated Development Environments (IDEs) and code editors, offering them information from its static type system. 

TypeScript also provides other rich features, such as autocompletion in an IDE, as well as type information, expected arguments, return types, etc., when hovering over a variable or function.

An IDE integrated with TypeScript has the added advantage of refactoring. For instance, when a variable name gets changed, the new name gets updated across the codebase due to TypeScript type checking. 

TypeScript improves the developer experience, and Vuex particularly benefits by helping to shape and structure the states using defined types, thereby improving the overall state management experience. 


## Setting up the environment

To integrate Vuex with TypeScript, you need to install [Vue](https://vuejs.org/) if you haven’t already, and then create a new Vue project using the following command:
``` 
# Install Vue CLI globally
npm install -g @vue/cli


# Create a new project
vue create my-vue-ts-project
```
You will be prompted to select features needed for your Vue project. Select the "Manually Select features" option, then select Vuex and TypeScript. This automatically bootstraps your application to use TypeScript and initializes a Vuex store for you on the fly.

After proceeding with the installation, navigate to your project with:
``` 
# Install Vue CLI globally
cd my-vue-ts-project
```
You can open the newly created folder in any IDE of your choice.

## Typescript Basics
Before proceeding to use TypeScript with Vue, understanding some basic concepts of TypeScript is essential. TypeScript shares a similar syntax with base JavaScript but adds extra features like static typing. This means the type of variables are defined on their initialization. This helps to prevent errors while you code. An explanation of some basic concepts is given below: 


### Custom Types
TypeScript enables you to define custom types that you can use in your application. This ensures your objects are strictly typed to any custom type you create. For example:
``` javascript
type Person = {
  name: string;
  age: number;
};

const personA: Person = {};
// Type '{}' is missing the following properties from type 'Person': name, age
```
Here you created a custom type `Person` and discovered that assigning a variable with type `Person` caused an error because an empty object does not have properties `name` and `age`. The correct code is shown below:
``` javascript
type Person = {
  name: string;
  age: number;
};

const personA: Person = {
  name: "John",
  age: 20,
};

console.log(personA.name, personA.age); // John, 20
```
TypeScript does not throw any errors if the variable with type `Person` has both properties `name` and `age`.

### Interfaces
Interfaces are similar to types, but a key difference is that an interface can be used to define classes while types can not. An example using a TypeScript interface is shown below:
``` javascript
interface Person {
  name: string;
  age: number;
  getName(): string;
}

class Student implements Person {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  getName() {
    return this.name;
  }

  getAge() {
    return this.age;
  }
}
const personA: Student = new Student("Nana", 20);
console.log(personA.getName(), personA.getAge());
```
In this example, the interface `Person` defines the class `Student`. Here, you created an instance of the `Student` class and used its methods to print the `name` and `age` properties.


### TypeScript Generics
Generics allow you to write reusable code that can be applied to different types that have the same shape. An example is shown below:
``` javascript
interface Shape {
  length: number;
  width: number;
}

class Rectangle implements Shape {
  length: number;
  width: number;
  constructor(length: number, width: number) {
    this.length = length;
    this.width = width;
  }
}

class Square implements Shape {
  length: number;
  width: number;
  constructor(length: number) {
    this.length = length;
    this.width = length;
  }
}

function getArea<T extends Shape>(shape: T): number {
  return shape.length * shape.width;
}

const rectangle: Shape = new Rectangle(10, 5);
const rectangleArea = getArea(rectangle);

const square: Shape = new Square(7);
const squareArea = getArea(square);

console.log(rectangleArea, squareArea); // 50, 49
```
In the above code, an interface `Shape` is defined. A generic function `getArea` was used to calculate the area of any type of `Shape`. Two separate classes, `Rectangle` and `Square`, were created, and they implemented the `Shape` interface (they are types of `Shape`). Therefore, the areas of `Rectangle` and `Square` instances can be calculated from a single `getArea` generic function.

Now that you have learned a few basic concepts of TypeScript, you will get started in applying these concepts to building a Vue application with Vuex state management.

## Getting Started
Vue-CLI automatically creates a `store` for you (if you selected `Vuex` as an additional feature when adding the project). Otherwise, create a store in the `src` directory and add an `index.ts` file. Install Vuex with `npm i vuex` as well. Replace the content of the `index.ts` with the following code:
``` javascript
import { createStore } from "vuex";
export interface State {
  count: number;
}

export default createStore<State>({
  state: { count: 0 },
  getters: {},
  mutations: {},
  actions: {},
  modules: {},
});
```
The above code creates an interface called `State`.  This defines the shape of the state object we use in the `createStore` function. The `createStore` function in Vuex represents the global state and how you can access it throughout the application. Notice the generic `createStore<State>` allowed you to define the shape of your state. Removing `count: 0` will throw an error because the `state` object will not match the `State` interface.

To use the `store` through Options API, go to `main.ts` and add the following code:
``` javascript
import { createApp } from "vue";
import App from "./App.vue";
import store, { State } from "./store";
import { Store } from "vuex";

declare module "@vue/runtime-core" {
  interface ComponentCustomProperties {
    $store: Store<State>;
  }
}
createApp(App).use(store).mount("#app");
```
The `declare module` redefines the `ComponentCustomProperties` of the Vue runtime. This is necessary to access the `$store` property in Vue components.

Replace the `HelloWorld.vue` and `App.vue` components with the following code:

`HelloWorld.vue`
``` javascript
<template>
  <div class="hello">
    <p>count: {{ count }}</p>
  </div>
</template>

<script lang="ts">
import { defineComponent } from "vue";

export default defineComponent({
  computed: {
    count(): number {
      return this.$store.state.count;
    },
  },
});
</script>
```
`App.vue`
``` javascript
<template>
  <HelloWorld />
</template>

<script lang="ts">
import HelloWorld from "./components/HelloWorld.vue";
import { defineComponent } from "vue";

export default defineComponent({
  components: { HelloWorld },
});
</script>
```
Run the server with `npm run serve`, and the `count` property from the state (which is currently 0) is displayed.

## Vuex Mutations
Mutations change the value of data stored in a Vuex state. Mutations are a set of functions that have access to the state data and can change it. Notice in the `store/index.ts`, you have a `mutations` object, which is currently empty. 

To use `mutations`, adjust the `store/index.ts` code to the following:
``` javascript
import { createStore } from "vuex";

export interface State {
  count: number;
}

export default createStore<State>({
  state: { count: 0 },
  getters: {},
  mutations: {
    increment(state: State) {
      state.count++;
    },
  },
  actions: {},
  modules: {},
});
```
The code above adds an `increment` mutation and has the `State` interface as a parameter. Calling the mutation updates the `count` property of the state. To use it in the `HelloWorld.vue` component, replace the code with the following:
``` javascript
<template>
  <div class="hello">
    <p>count: {{ count }}</p>
    <button @click="increment">Increase me</button>
  </div>
</template>

<script lang="ts">
import { defineComponent } from "vue";

export default defineComponent({
  computed: {
    count(): number {
      return this.$store.state.count;
    },
  },

  methods: {
    increment() {
      this.$store.commit("increment");
    },
  },
});
</script>
```
The `increment` method of the `HelloWorld.vue` component commits the `increment` mutation of the Vuex store anytime it is called. You attached this method to the `click` event of the button in the template. Anytime the button is clicked, the value of the `count` property in the store is updated.

## Vuex Actions
Vuex actions are a set of methods that enable you to update the values of a Vuex store asynchronously. Vuex mutations are synchronous by design, and it is not advisable for functions in Vuex mutations to be asynchronous. 
To create a Vuex action, input the following code in your  `store/index.ts`:
``` javascript
import { createStore } from "vuex";

export interface State {
  count: number;
}

export default createStore<State>({
  state: { count: 0 },
  getters: {},
  mutations: {
    increment(state: State) {
      state.count++;
    },
  },
  actions: {
    incrementAsync({ commit }) {
      setTimeout(() => commit("increment"), 1000);
    },
  },
  modules: {},
});
```
This adds an `incrementAsync` function to the `actions` object. It uses `setTimeout` to call the `increment` action after one second. The `{ commit }` deconstructed the `store` parameter supplied to Vuex actions. This allowed a shorter way to commit to the state.

To use the action, replace the `HelloWorld.vue` component with the following:
``` javascript
<template>
  <div class="hello">
    <p>count: {{ count }}</p>
    <button @click="increment">Increase me</button>
  </div>
</template>

<script lang="ts">
import { defineComponent } from "vue";

export default defineComponent({
  computed: {
    count(): number {
      return this.$store.state.count;
    },
  },

  methods: {
    increment() {
      this.$store.dispatch("incrementAsync");
    },
  },
});
</script>
```
You replaced the `increment` function to use the Vuex action instead of committing to the state directly. You will notice that clicking the button updates the `count` in the state after 1 second.



<CTA_Middle_Frameworks />



## Vuex Getters
Vuex getters allow us to compute a derived state from the original state. They are read-only helper functions that allow us to derive more information about our original state. To use Vuex getters, add the following code to `store/index.ts`:
``` javascript
import { GetterTree, createStore } from "vuex";

export interface Getters extends GetterTree<State, State> {
  doubleCount(state: State): number;
  isEven(state: State): boolean;
}

const getters: Getters = {};

// Type '{}' is missing the following properties from type 'Getters': doubleCount, isEven
```
The code above defines an interface for your getters. It takes advantage of TypeScript's strong typing to make sure your getters are defined correctly. There will be an error because the `getters` object has not been completely implemented to match the `getters` interface. Complete the code with the following:
``` javascript
import { GetterTree, createStore } from "vuex";

export interface State {
  count: number;
}

export interface Getters extends GetterTree<State, State> {
  doubleCount(state: State): number;
  isEven(state: State): boolean;
}

const getters: Getters = {
  doubleCount(state: State) {
    return state.count * 2;
  },
  isEven(state: State) {
    return state.count % 2 == 0;
  },
};

export default createStore({
  state: { count: 0 },
  getters,
  mutations: {
    increment(state: State) {
      state.count++;
    },
  },
  actions: {
    incrementAsync({ commit }) {
      setTimeout(() => commit("increment"), 1000);
    },
  },
  modules: {},
});
```
The code implements the `getters` object and sets it as the Vuex getters in the `createStore` getters. Proceed to use it in the `HelloWorld.vue` component with the code shown below:
``` javascript
<template>
  <div class="hello">
    <p>count: {{ count }}</p>
    <p>is even: {{ isEven }}</p>
    <p>double of count: {{ double }}</p>
    <button @click="increment">Increase me</button>
  </div>
</template>

<script lang="ts">
import { defineComponent } from "vue";

export default defineComponent({
  computed: {
    count(): number {
      return this.$store.state.count;
    },
    isEven(): boolean {
      return this.$store.getters.isEven;
    },
    double(): number {
      return this.$store.getters.doubleCount;
    },
  },

  methods: {
    increment() {
      this.$store.commit("increment");
    },
  },
});
</script>
```
This adds extra computed methods that return the getter functions. `isEven` determines if the `count` state is an even number, and the `doubleCount` computes double the value of count.

## Vuex Modules
Modules allow the separation of parts of the state and allow different logic to be segmented. It also prevents the state object from becoming large and hard to maintain. To use Vuex modules, proceed with the following example:

Consider a scenario in which you want to build a minimal social media application. To manage the state of the users, posts, and comments, you could have a Vuex configuration like the following:
``` javascript
import { createStore } from "vuex";

interface User {
  name: string;
}

interface Post {
  id: string;
  title: string;
  content: string;
}

interface Comment {
  postId: string;
  comment: string;
}
export interface State {
  user: User | null;
  posts: Post[];
  comments: Comment[];
}

export default createStore<State>({
  state: { user: null, posts: [], comments: [] },
  getters: {},
  mutations: {
    setUser(state: State, user: User) {
      state.user = user;
    },
    addPost(state: State, post: Post) {
      state.posts.push(post);
    },
    addComment(state: State, comment: Comment) {
      state.comments.push(comment);
    },
  },
  actions: {
    login({ commit }, user) {
      // Simulate user login
      commit("setUser", user);
    },
    createPost({ commit }, post) {
      // Simulate creating a post
      commit("addPost", post);
    },
    createComment({ commit }, comment) {
      // Simulate creating a comment
      commit("addComment", comment);
    },
  },
});
```
The `state`, `actions`, and `mutations` already look bulky even without real implementations. Vuex modules help solve this issue. A refactored code using Vuex modules is shown below:
``` javascript
import { Module, createStore } from "vuex";

interface User {
  name: string;
}

interface Post {
  id: string;
  title: string;
  content: string;
}

interface Comment {
  postId: string;
  comment: string;
}

export interface State {
  user: User | null;
  posts: Post[];
  comments: Comment[];
}

export interface UserModuleState {
  user: User | null;
}

export interface PostModuleState {
  posts: Post[];
}

export interface CommentModuleState {
  comments: Comment[];
}

const userModule: Module<UserModuleState, State> = {
  state: () => ({ user: null }),
  mutations: {
    setUser(state: UserModuleState, user: User) {
      state.user = user;
    },
  },
  actions: {
    login({ commit }, user) {
      // Simulate user login
      commit("setUser", user);
    },
  },
};

const postModule: Module<PostModuleState, State> = {
  state: () => ({ posts: [] }),
  mutations: {
    addPost(state: PostModuleState, post: Post) {
      state.posts.push(post);
    },
  },
  actions: {
    createPost({ commit }, post) {
      // Simulate creating a post
      commit("addPost", post);
    },
  },
};

const commentModule: Module<CommentModuleState, State> = {
  state: () => ({ comments: [] }),
  mutations: {
    addComment(state: CommentModuleState, comment: Comment) {
      state.comments.push(comment);
    },
  },
  actions: {
    createComment({ commit }, comment) {
      // Simulate creating a comment
      commit("addComment", comment);
    },
  },
};

export default createStore<State>({
  modules: {
    userModule,
    postModule,
    commentModule,
  },
});
```
You would observe that the logic for `user`, `post`, and `comments` have been separated into different `Modules`. Each with its `state`, `actions`, and `mutations`.

It is recommended to store each module in its own separate file to promote better separation of concerns and smaller, closely related compact code for each module.

Vuex modules can also contain inner modules, and a lot can be explored with this powerful feature in the [Official Vuex Documentation](https://vuex.vuejs.org/guide/)

## Common patterns used in Vuex
Explore some best practices and practical strategies for enhancing your TypeScript code. These tips will guide you into more maintainable TypeScript development. 

### Helper functions
The main `store` does not have to contain the functions for your `actions` and `mutations`. Helper functions for `actions`, `mutation`, or `getters` can be separated into different modules and imported from there.

### Vuex Mappers 
Instead of adding `methods` in a component for each action or mutation, Vuex provides helper functions that map `actions`, `mutations`, or `getters` directly to a component's `methods` or `computed`. In the previous examples, we called the `dispatch` or `commit` methods of the store in our component's `methods` or computed `object`. 
``` javascript
import { createStore } from "vuex";

export interface State {
  count: 0;
}

export default createStore<State>({
  state: { count: 0 },
  mutations: {
    increment(state: State) {
      state.count++;
    },
  },
});
```
This code is the previous example of setting up a Vuex store, but you will use Vuex helpers called `mapMutations` and `mapState` in the `HelloWorld.vue` component, as shown below:
``` javascript
<template>
  <div class="hello">
    <p>count: {{ count }}</p>
    <button @click="increment">Increase me</button>
  </div>
</template>

<script lang="ts">
import { defineComponent } from "vue";
import { mapMutations, mapState } from "vuex";

export default defineComponent({
  computed: {
    ...mapState(["count"]),
  },
  methods: {
    ...mapMutations(["increment"]),
  },
});
</script>
```
Instead of creating a computed property to access the state with `this.$store.state`, you used a Vuex helper called `mapState` to map it directly in the computed object. You specified the name of the state property you wanted to access (`count`) as a string in a list, which was added to the `mapState` function as an argument.

Similarly, you did the same for the `increment` mutation function with Vuex `mapMutations`.

## Potential Pitfalls and solutions
TypeScript ensures better code practice. You might run into issues like `TypeErrors` where a value you want to use does not match a type from a function you need. A quick solution would be to specify your type as `any`, which will allow any type to be used. Be careful not to use it too much; rather, ensure clear interface definitions.

## Conclusion

In this article, you explored various ways of integrating TypeScript with Vuex and observed the benefits of TypeScript's strongly typed system and how it helps prevent errors before they occur. You also got familiar with what a Vuex store is, along with `states`, `mutations`, `actions`, and `getters`. 

Finally, you learned how to split your state management system if the need arises with Vuex modules.

This article serves as a platform for you to build cleaner and more robust applications with Vuex. Using TypeScript serves as a powerful tool to eradicate errors before they become a major problem.

You are encouraged to explore more about Vuex in their [Official Documentation](https://vuex.vuejs.org/guide) alongside TypeScript to leverage its benefit by building more projects as you go along.