How to use TypeScript to Create Vue Apps with Vue Class Components
Tags: vue, typescript, tutorial, webdev
Published: 2021-02-08 06:08:47

_by author [John Au-Yeung
](https://dev.to/aumayeung)_

Vue.js is a popular, easy to use, front end web development framework.

There are several ways we can use it to create Vue.js components.

The most common way is to use the options API, which lets us create components with an object that we export.

An alternative way is to use the `vue-class-component` library to create class-based components.

Class-based components work better with TypeScript because the value of `this` is the component class instance. And this is consistent through the component class.

The `vue-class-component` library is only compatible with Vue.js 2.

In this article, we'll look at how to create Vue.js apps with class-based components.

## Introducing Vue Class Based Components

We can start using class-based components by creating a project with the Vue CLI.

To do this, we run:

```
$ vue create app
```

In the Vue CLI screen, we choose ‘Manually select features’, then choose Vue 2, and then choose TypeScript in the ‘Check the features needed for your project’ question.

Then when we see the 'Use class-style component syntax?' question, we choose Y.

Alternatively, we can install the `vue-class-component` package with:

```
$ npm install --save vue vue-class-component
```

with NPM or:

```
$ yarn add --save vue vue-class-component
```

with Yarn.

Then we enable the `experimentalDecorators` option in `tsconfig.json`:

```json
{
  "compilerOptions": {
    ...
    "target": "es5",
    "module": "es2015",
    "moduleResolution": "node",
    "strict": true,
    "experimentalDecorators": true,
    "esModuleInterop": true
    ...
  }
}
```

Once we set up the project, we can create a simple app with it.

For instance, we can create a counter app by writing:

`App.vue`

```vue
<template>
  <div id="app">
    <Counter />
  </div>
</template>

<script>
import Counter from "./components/Counter";

export default {
  name: "App",
  components: {
    Counter,
  },
};
</script>
```

`src/Counter.vue`

```vue
<template>
  <div>
    <button v-on:click="decrement">decrement</button>
    <button v-on:click="increment">increment</button>
    <p>{{ count }}</p>
  </div>
</template>

<script lang="ts">
import Vue from "vue";
import Component from "vue-class-component";

@Component
export default class Counter extends Vue {
  count = 0;

  public increment(): void {
    this.count++;
  }

  public decrement(): void {
    this.count--;
  }
}
</script>
```
The key takeaways from the example are:

We create the `Counter` class that's a subclass of the `Vue` class.

We turn it into a component with the `Component` decorator.

`count` is class property, and it's a reactive property.



We shouldn't initialize reactive properties in `constructor` because class initialization doesn't fit in the Vue component lifecycle.

Then `increment` and `decrement` methods are methods we can call in the template as we can see from the `Counter.vue` component.

We access `count` with `this.count` as usual.

## Class Based Components Hooks and Mixins

The usual component hooks can be added into class-based components.

For instance, we can write:

```vue
<template>
  <div></div>
</template>

<script>
import Vue from "vue";
import Component from "vue-class-component";

@Component
export default class HelloWorld extends Vue {
  mounted() {
    console.log("mounted");
  }
}
</script>
```

We added the `mounted` hooks into our `HelloWorld` component.

And we should see the `console.log` run when we mount the component.

Other important hooks include:

beforeCreate - runs when component instance is initialized
created - runs after the component instance is created
beforeUpdate - runs when data changes but before the DOM changes are applied
updated - runs after DOM changes are applied
beforeDestroyed - runs right before the component is unmounted
destroyed - runs after the component is destroyed

To compile the app to something that users can use in the browser, we run `npm run build`.

To add mixins, we can create another component class and then call the `mixins` method to return a mixin class that we can use as a subclass of a class-based component.

For instance, we can write:

```vue
<template>
  <div>hi, {{ firstName }} {{ lastName }}</div>
</template>

<script>
import Vue from "vue";
import Component, { mixins } from "vue-class-component";

@Component
class FirstName extends Vue {
  firstName = "jane";
}

@Component
class LastName extends Vue {
  lastName = "smith";
}

@Component
export default class HelloWorld extends mixins(FirstName, LastName) {
  mounted() {
    console.log(this.firstName, this.lastName);
  }
}
</script>
```

We call the `mixins` function with the component classes we want to incorporate as parts of the mixin.

Then we can access the items from the mixin classes in the `HelloWorld` class.

Therefore, when we add `firstName` and `lastName` in the template, their values will be displayed.

And we see 'hi, jane smith' displayed.

In the hooks and methods, we can also get their values as we did in the `mounted` hook.

## Using Vue with TypeScript and why should we use it?

We should use Vue with TypeScript because it lets us annotate the structure of various entities like props, refs, hooks, and more.

Also, methods and reactive properties can have their types annotated.

This means that we get compile-time type checking and we can avoid looking up the contents of objects by logging or checking documentation.

It also avoids data type errors if we have typos and other mistakes.

For example, we can write:

`src/HelloWorld.vue`

```vue
<template>
  <div>{{ message }}</div>
</template>

<script lang="ts">
import Vue from "vue";
import Component from "vue-class-component";

const HelloWorldProps = Vue.extend({
  props: {
    name: String,
  },
});

@Component
export default class HelloWorld extends HelloWorldProps {
  get message(): string {
    return `hi ${this.name}`;
  }
}
</script>
```

We defined our props by using the `Vue.extend` method with the `props` property.

`name` is the prop.

Then our `HelloWorld` component extends `HelloWorldProps` instead of `Vue` so that we can add the props to the `HelloWorld` component.

We set the return type of the `message` getter method to `string`.

`message` is a computed property.

The `lang` is set to `ts` so we can use TypeScript to write our components.

This is required so Vue CLI can build with the correct compiler.

Also, if modules aren't working, then we need the `esModuleInterop` option in the `compilerOptions` property in `tsconfig.json` so the TypeScript compiler can import ES modules.

Then we can use the component by writing:

`App.vue`

```vue
<template>
  <div id="app">
    <HelloWorld name="jane" />
  </div>
</template>

<script>
import HelloWorld from "./components/HelloWorld";

export default {
  name: "App",
  components: {
    HelloWorld,
  },
};
</script>
```

We can define types for reactive properties by writing:

```vue
<template>
  <div>{{ message }}</div>
</template>

<script lang="ts">
import Vue from "vue";
import Component from "vue-class-component";

interface Person {
  firstName: string;
  lastName: string;
}

@Component
export default class HelloWorld extends Vue {
  persons!: Person[] = [
    { firstName: "jane", lastName: "smith" },
    { firstName: "bob", lastName: "jones" },
  ];

  get message(): string {
    const people = this.persons
      .map(({ firstName, lastName }) => `${firstName} ${lastName}`)
      .join(", ");
    return `hi ${people}`;
  }
}
</script>
```

We assign an array with the `firstName` and `lastName` properties to the `persons` class property.

We need both properties because the `Person` interface has `firstName` and `lastName` in there and we didn't specify that they're optional.

The `!` means the class property isn't nullable.

Then we use `this.persons` in the `message` getter to put all the names in the `people` string.

To add type annotations for methods, we write:

```vue
<template>
  <div>{{ message }}</div>
</template>

<script lang="ts">
import Vue from "vue";
import Component from "vue-class-component";

interface Person {
  firstName: string;
  lastName: string;
}

@Component
export default class HelloWorld extends Vue {
  persons!: Person[] = [
    { firstName: "jane", lastName: "smith" },
    { firstName: "bob", lastName: "jones" },
  ];
  message: string = "";

  getMessage(greeting: string): string {
    const people = this.persons
      .map(({ firstName, lastName }) => `${firstName} ${lastName}`)
      .join(", ");
    return `${greeting} ${people}`;
  }

  mounted() {
    this.message = this.getMessage("hi");
  }
}
</script>
```

We have the `getMessage` method with the `greeting` parameter which is set to be a string.

The return type of the method is also a string.

To add data types for refs, we write:

```vue
<template>
  <input ref="input" />
</template>

<script lang="ts">
import Vue from "vue";
import Component from "vue-class-component";

@Component
export default class Input extends Vue {
  $refs!: {
    input: HTMLInputElement;
  };

  mounted() {
    this.$refs.input.focus();
  }
}
</script>
```

We have an input element which assigned the `input` ref in the template by setting the `ref` prop.

And then we add the data type to the `$refs.input` class property.

So we set the type of `this.$refs.input` to be `HTMLInputElement`.

And as a result, we should see the `focus` method, since it’s part of the public API of this type.

## SampleApp with Vue Class Based Component and TypeScript

To show you a full example of how to use TypeScript to create a complete Vue App, we can create our own recipe app with class based components.

To do this, we create a TypeScript Vue 2 project with Vue as we did at the beginning of this article.

Then we install the `uuid` package to let us create unique IDs for the recipe entries along with the type definitions.

We can install them by running:

```
$ npm i uuid @types/uuid
```

with NPM or:

```
$ yarn add uuid @types/uuid
```

to add them with Yarn.

Then we can create the `Recipe` interface in `src/interfaces/Recipe.ts`:

```ts
export interface Recipe {
  id: string;
  name: string;
  ingredients: string;
  steps: string;
}
```

Next, we create the `RecipeForm.vue` in the `src/components` folder:

`src/components/RecipeForm.vue`:

```vue
<template>
  <div>
    <form @submit.prevent="addRecipe">
      <div>
        <label>Name</label>
        <br />
        <input v-model="recipe.name" />
        <br />
        {{ !recipe.name ? "Name is required" : "" }}
      </div>
      <div>
        <label>Ingredients</label>
        <br />
        <textarea v-model="recipe.ingredients"></textarea>
        <br />
        {{ !recipe.ingredients ? "Ingredients is required" : "" }}
      </div>
      <div>
        <label>Steps</label>
        <br />
        <textarea v-model="recipe.steps"></textarea>
        <br />
        {{ !recipe.steps ? "Steps is required" : "" }}
      </div>
      <button type="submit">Add Recipe</button>
    </form>
    <div v-for="(r, index) of recipes" :key="r.id">
      <h1>{{ r.name }}</h1>
      <h2>Ingredients</h2>
      <div class="content">{{ r.ingredients }}</div>
      <h2>Steps</h2>
      <div class="content">{{ r.steps }}</div>
      <button type="button" @click="deleteRecipe(index)">Delete Recipe</button>
    </div>
  </div>
</template>

<script lang="ts">
import Vue from "vue";
import Component from "vue-class-component";
import { Recipe } from "../interfaces/Recipe";
import { v4 as uuidv4 } from "uuid";

@Component
export default class RecipeForm extends Vue {
  recipe: Recipe = {
    id: "",
    name: "",
    ingredients: "",
    steps: "",
  };
  recipes: Recipe[] = [];

  get formValid(): boolean {
    const { name, ingredients, steps } = this.recipe;
    return Boolean(name && ingredients && steps);
  }

  addRecipe() {
    if (!this.formValid) {
      return;
    }
    this.recipes.push({
      id: uuidv4(),
      ...this.recipe,
    } as Recipe);
  }
  deleteRecipe(index: number) {
    this.recipes.splice(index, 1);
  }
}
</script>

<style scoped>
.content {
  white-space: pre-wrap;
}
</style>
```

Then in `App.vue`, we write:

```vue
<template>
  <div id="app">
    <RecipeForm />
  </div>
</template>

<script>
import RecipeForm from "./components/RecipeForm";

export default {
  name: "App",
  components: {
    RecipeForm,
  },
};
</script>
```

to render the `RecipeForm` in our app.

In `RecipeForm.vue`, we have a form with the input and `textarea` elements to let us enter the recipe entries.

The `@submit` directive lets us listen to the `submit` event which is triggered when we click on the Add Recipe button.

The `prevent` modifier lets you trigger client-side form submission instead of server-side submission.

The `v-model` directive lets us bind the inputted values to the properties of the `recipe` reactive property.

Below the input and text areas, we show an error message if the field doesn't have a value.

Below that, we render the `recipes` entries and in each entry, we have a Delete Recipe button to delete the entry.

The `key` prop set to `r.id`, which is set to a unique ID so Vue can identify the entry.

In the script tag, we have the `RecipeForm` component which is a subclass of `Vue` like a typical Vue class component.

Inside it, we define the `recipe` property, which has the `Recipe` type.

We define the initial values of each property.

Also, we have the `recipes` array which is just an array of objects that implement the `Recipe` interface.

Then we have the `formValid` computed property, which we defined as a getter.

We just check if each property of `this.recipe` has a value.

In the `addRecipe` method, we check the `formValid` reactive property to see if all the items are filled in.

If it's `true`, then we call `this.recipes.push` to push an item into the `this.recipes` array.

The `uuidv4` function returns a unique ID.

The `deleteRecipe` method takes an `index` parameter, which is a number, then we call `splice` to remove the entry with the given `index`.

In the end, we have something that looks like this:

![Recipe app](https://asayer-content.s3.eu-central-1.amazonaws.com/a1ad4cf1f53340ada023242c62bd39f8.png)

## Frontend Monitoring
TypeScript is a great tool to keep common mistakes out, by providing a way to perform static type checks (during the compilation phase), however, there is no substitute to active monitoring of an application.
That being said, debugging a web application in production may be challenging and time consuming. Asayer is a frontend monitoring tool that replays everything your users do and shows how your app behaves for every issue. It’s like having your browser’s inspector open while looking over your user’s shoulder.
Asayer lets you reproduce issues, aggregate JS errors and monitor your app’s performance. Asayer offers plugins for capturing the state of your *Redux or VueX* store and for inspecting *Fetch* requests and *GraphQL* queries.
![text](https://asayer-content.s3.eu-central-1.amazonaws.com/d9ddc1e0b6ad4f3f8a322bb0ac8c6a8d.png)
Happy debugging, for modern frontend teams - [Start monitoring your web app for free](https://asayer.io/register.html).

## Conclusion

We can use Vue class components library to add components into our app.

We can also define TypeScript types for each part of a component easily with class components.

We can define TypeScript types for parts of components like methods, refs, class properties, hooks, and more.