Back

How to use TypeScript to Create Vue Apps with Vue Class Components

How to use TypeScript to Create Vue Apps with Vue Class Components

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:

{
  "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

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

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

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

src/Counter.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:

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

<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

<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

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

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

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

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

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:

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

<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

Open Source Session Replay

OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.

replayer.png

Start enjoying your debugging experience - start using OpenReplay for free.

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.