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