Editor’s note: This article was last updated on 17 August 2022 to include new features introduced in Vue 3, including defineComponent.
Vue is an amazing lightweight, progressive frontend framework. In this tutorial, we’ll demonstrate how to build a Vue app completely in TypeScript. We’ll highlight some of the benefits of using TypeScript with Vue by building an example app with class-based components, Vuex for state management, lifecycle hooks, and more. Let’s get started!
Vue and TypeScript for beginners | Tutorial
Learn how to use Vue.js with TypeScript in this tutorial by frontend developer and content creator, @TylerPotts. You can find the original blog post here on the LogRocket blog: https://blog.logrocket.com/vue-typescript-tutorial-examples/?youtube-tutorial 00:00 LogRocket intro 00:15 Using Vue.js and TypeScript 00:45 Adding TypeScript to Vue.js 05:59 Using lifecycle Hooks 06:49 Using Vuex with TypeScript 10:30 Using class-based components #LogRocket #Vue #TypeScript LogRocket is a frontend monitoring solution that combines session replay, performance monitoring, and product analytics to help software teams create an ideal product experience.
defineComponentemitThe Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
Vue is flexible, so users are not forced to use TypeScript. Unlike Angular, older versions of Vue did not have proper support for TypeScript. For this reason, most Vue applications have historically been written in JavaScript.
Vue 3, released in September 2020, includes features like a built-in composition API, multiple root elements, and, crucially, improved TypeScript support. For detailed information on how to migrate your existing Vue projects to Vue 3, I recommend checking out Refactoring your Vue 2 apps to Vue 3.
Now that Vue officially supports TypeScript, it’s easy to create TypeScript projects from scratch using only the Vue CLI without any third-party libraries. However, neither the official Vue docs nor the TypeScript docs include all the information you need to get started. To paint a fuller picture, we’ll demonstrate how to build a new Vue and TypeScript application using the Vue CLI.
First, we’ll set up a new Vue project with TypeScript using the code below:
npx @vue/cli create typescript-app
Choose manually select features and configure it with the following settings:

Once the project is set up, run the project to test it:
cd typescript-app npm run serve
Open localhost:8080 or whatever URL your console shows after starting the project, and you should see your app running successfully. For each item below, I’ll show both the TypeScript and JavaScript-equivalent code so you can easily compare the two. Let’s get started!
defineComponentdefineComponent provides decorators from the Vue library, which we can use to define Vue components with full TypeScript support. To use defineComponent, open App.vue from the src folder and add the following code:
//src/App.vue
//Typescript code
<script lang="ts">
import { defineComponent } from 'vue';
import HelloWorld from './components/HelloWorld.vue';
export default defineComponent({
components:{
HelloWorld
}
})
</script>
To use TypeScript, we first need to set the lang attribute in the <script> tag to ts:
<script lang="ts">
defineComponent has a decorator that allows us to define the components and attributes of each component. The components object helps us to add our components into the template:
components: {
HelloWorld,
}
To use data properties, we can simply declare them as class variables:
export default defineComponent({
components:{
HelloWorld,
},
data(){
return{
title:"welcome to my app",
list: [
{
name: 'popoolatopzy',
age: '26'
},
{
name: 'Preetish',
age: '26'
},
{
name: 'John',
age: '30'
}
]
}
}
})
The JavaScript-equivalent code would look like the following:
export default {
data() {
return {
title: "welcome to my app",
list: [
{
name: 'Preetish',
age: '26'
},
{
name: 'John',
age: '30'
}
]
}
}
To use props in our Vue component, we can use the props decorator. In Vue, we can give additional details for props, like required, default, and type. However, since we’re using defineComponent, we don’t need to import the Prop decorator. Instead, we can write props in defineComponent, as shown below. We could also use readonly to avoid manipulating the props:
export default defineComponent({
components:{
HelloWorld
},
data(){
return{
title:"welcome to my app",
}
},
props: {
name:{
readonly:true,
default: 'John doe',
type:String
},
job:{
required: false,
type: String,
default: 'Developer'
}
},
setup(props) {
props.name,
props.age,
props.job
},
})
The equivalent JavaScript code is below:
export default {
props: {
name: {
default: 'John doe'
},
age: {
required: true,
},
job: {
required: false,
type: string,
default: 'Developer'
}
}
}
We can use a computed property to write simple template logic, like manipulating, appending, or concatenating data. In TypeScript, a normal computed property is also prefixed with the get keyword:
export default defineComponent({
components:{
HelloWorld
},
data(){
return{
title:"welcome to my app",
first:'Popoola',
last:"Temitope"
}
},
computed:{
fullName(): String{
return this.first+ ' '+ this.last
}
}
})
Below is the JavaScript equivalent:
export default {
fullName() {
return this.first + ' ' + this.last
}
}
We can write complex computed properties, which have both getter and setter. By default, computed properties are getter-only. The getter and setter must be used in the computed property for TypeScript as follows:
export default defineComponent({
components:{
HelloWorld
},
title:"welcome to my app",
data(){
return{
title:"welcome to my app",
first:'Popoola',
last:"Temitope"
}
},
computed:{
fullname:{
// getter
get() : string {
return this.first+" "+ this.last
},
// setter
set(value : string) {
let names = value.split(' ');
this.first = names[0];
this.last = names[names.length - 1]
}
}
}
})
The JavaScript-equivalent code is shown below:
fullName: {
get: function () {
return this.first + ' ' + this.last
},
set: function (newValue) {
let names = newValue.split(' ')
this.first = names[0]
this.last = names[names.length - 1]
}
}
Methods in TypeScript, like normal class methods, have an optional access modifier:
export default defineComponent({
components:{
HelloWorld
},
data(){
return{
title:"welcome to my app",
}
},
methods:{
clickMe() {
console.log('clicked')
console.log(this.addNum(4, 2))
},
addNum(num1: number, num2: number): number {
return num1 + num2
}
}
})
The JavaScript-equivalent code is as follows:
export default {
methods: {
clickMe() {
console.log('clicked')
console.log(this.addNum(4, 2))
}
addNum(num1, num2) {
return num1 + num2
}
}
}
In TypeScript, Watchers are written differently than how we’d usually write them in JavaScript. The most commonly used syntax for a watcher in JavaScript is below:
watch: {
name: function(newval) {
//do something
}
}
We don’t tend to use the handler syntax often:
watch: {
name: {
handler: 'nameChanged'
}
},
methods: {
nameChanged (newVal) {
// do something
}
}
The TypeScript syntax is similar to the second method. In TypeScript, we use the Watch decorator and follow the name of the variable we need to watch:
watch: {
name: function(newValue,oldValue) {
//do something
}
}
We can also set the immediate and deep watchers:
watch: {
someObject: {
handler(newValue, oldValue) {
//do something
},
deep: true,
immediate:true,
}
}
Below is the JavaScript equivalent code:
watch: {
person: {
handler: 'projectChanged',
immediate: true,
deep: true
}
}
methods: {
projectChanged(newVal, oldVal) {
// do something
}
}
emitTo emit a method from a child component to a parent component, we’ll use the emit decorator in TypeScript:
emits: ['increment'],
setup(props, { emit }) {
const addToCount=(n: number)=>{
this.count += n
}
},
emits: ['resetData'],
setup(props, { emit }) {
const resetCount=(n: number)=>{
this.count = 0
}
},
In the first example, the function called addToCount is converted to kebab case, which is very similar to how emit works in Vue.
In the second example, we pass the explicit name resetData for the method, and we then use that name instead. Since addData is in camel case, we convert it to kebab case instead:
<some-component add-to-count="someMethod" />
<some-component reset-data="someMethod" />
//Javascript Equivalent
methods: {
addToCount(n) {
this.count += n
this.$emit('add-to-count', n)
},
resetCount() {
this.count = 0
this.$emit('resetData')
}
}
Inside the @components decorator, we store the code to register components inside the other components. Let’s demonstrate this by creating a counter app. Add the following code to App.vue:
<template>
<div class="app">
<h1>{{title}}</h1>
</div>
<Counter/>
</template>
//Typescript code
<script lang="ts">
import { defineComponent } from 'vue';
import Counter from './components/Counter.vue';
export default defineComponent({
components:{
Counter
},
data(){
return{
title:"welcome to my counter app",
}
}
})
</script>
To use a counter component, create Counter.vue inside the components folder and add the following code to it:
<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 { defineComponent} from 'vue';
export default defineComponent({
components:{},
data(){
return{
count:0
}
},
methods:{
increment() {
this.count++;
},
decrement() {
this.count--;
}
}
})
</script>
Vuex is the official state management library used in most Vue applications. In Vuex, it’s good practice to split the store into modules, so we’ll demonstrate how to write that in TypeScript. To store the state, let’s create a file called types.ts in the store folder:
// store/types.ts
export interface RootState{
stateTitle:string;
}
In the store folder, we need to create an index.ts file to initialize Vuex and register the module:
// store/index.ts
import Vue from 'vue';
import Vuex, { StoreOptions } from 'vuex';
import { RootState } from './types';
const store:StoreOptions<RootState> = {
state:{
stateTitle:"Vue.js and TypeScript: A Complete Tutorial With Examples",
},
modules:{
}
}
export default new Vuex.Store<RootState>(store)
In the code above, we utilize StoreOptions from the Vuex library to handle the State, Getter, Mutation, and Action. We can access and update the value for stateTitle using Getter and Setter in the App.vue file as follows:
// src/App.vue
<template>
<div id="app">
<h1>{{stateTitle}}</h1>
<br><br>
<Counter />
</div>
</template>
<script lang="ts">
import Counter from './components/Counter.vue';
import store from './store';
import { defineComponent} from 'vue';
export default defineComponent({
title:"Counter App",
components: {
Counter,
},
computed:{
stateTitle:{
get():string{
return store.state.stateTitle;
},
set(value:string){
store.state.stateTitle = value
console.log(store.state.stateTitle)
}
}
}
})
</script>
A Vue component has eight lifecycle hooks, including created and mounted. We use the same TypeScript syntax for each hook, and we declare these as normal class methods. Since lifecycle hooks are called automatically, they neither take an argument nor return any data. Therefore, we don’t need access modifiers, typing arguments, or return types:
export default defineComponent({
mounted() {
//do something
},
beforeUpdate() {
// do something
}
})
The JavaScript-equivalent code is shown below:
export default {
mounted() {
//do something
},
beforeUpdate() {
// do something
}
}
To create mixins in TypeScript, we must first create our mixin file, which contains the data we share with other components.
Inside the mixins directory, create a file called ProjectMixin.ts and add the following mixin, which shares the project name and a method to update the project name:
// src/mixins/ProjectMixin.ts
import { defineComponent } from 'vue';
export default defineComponent({
data(){
return{
projName:"Vue.js and TypeScript: A complete tutorial with examples",
}
},
projName:'My project',
projectDetail1: {
set value(v : string) {
this.projName = v;
},
get value() : string {
return this.projName
}
}
})
In JavaScript, we’d write the code above as follows:
export default {
data() {
return {
projName: 'My project'
}
},
methods: {
setProjectName(newVal) {
this.projName = newVal
}
}
}
To use the TypeScript mixin above in our Vue component, we need to import the Mixins from Vue Property Decorator as well as our mixin file itself, then write it as follows:
// src/App.vue
<template>
<div>
1{{ projectDetail }}
</div>
</template>
//Typescript code
<script lang="ts">
import { defineComponent } from 'vue';
import HelloWorld from './components/HelloWorld.vue';
import ProjectMixin from '@/mixins/ProjectMixin'
export default defineComponent({
components:{
HelloWorld,
ProjectMixin
},
mixins: [ ProjectMixin ],
computed: {
projectDetail() {
return this.projName + ' ' + 'Preetish HS'
}
}
})
</script>
The JavaScript-equivalent code would be as follows:
<template>
<div class="project-detail">
{{ projectDetail }}
</div>
</template>
<script>
import ProjectMixin from '@/mixins/ProjectMixin'
export default {
mixins: [ ProjectMixin ],
computed: {
projectDetail() {
return this.projName + ' ' + 'Preetish HS'
}
}
}
</script>
In this article, we’ve covered all the basic information you need to create a Vue application completely in TypeScript with custom decorator features and no third-party libraries. Now, you should be able to get your Vue app up and running in TypeScript with features like defineComponent, data, props, computed properties, methods, and watchers.
Vue 3.0 includes better support for TypeScript out of the box, and the entire Vue code was rewritten in TypeScript to improve maintainability. I hope you enjoyed this tutorial, and be sure to leave a comment if you have any questions. Happy coding!
Debugging Vue.js applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Vue mutations and actions for all of your users in production, try LogRocket.

LogRocket lets you replay user sessions, eliminating guesswork by showing exactly what users experienced. It captures console logs, errors, network requests, and pixel-perfect DOM recordings — compatible with all frameworks.
With Galileo AI, you can instantly identify and explain user struggles with automated monitoring of your entire product experience.
Modernize how you debug your Vue apps — start monitoring for free.
LogRocket lets you replay user sessions, eliminating guesswork by showing exactly what users experienced. It captures console logs, errors, network requests, and pixel-perfect DOM recordings — compatible with all frameworks, and with plugins to log additional context from Redux, Vuex, and @ngrx/store.
With Galileo AI, you can instantly identify and explain user struggles with automated monitoring of your entire product experience.
Modernize how you understand your web and mobile apps — start monitoring for free.

This post walks through a complete six-step image optimization strategy for React apps, demonstrating how the right combination of compression, CDN delivery, modern formats, and caching can slash LCP from 8.8 seconds to just 1.22 seconds.

Learn what vinext is, how Cloudflare rebuilt Next.js on Vite, and whether this experimental framework is worth watching.

Memory leaks in React don’t crash your app instantly, they quietly slow it down. Learn how to spot them, what causes them, and how to fix them before they impact performance.

Build agent-ready websites with Google Web MCP. Learn how to expose reliable site actions for AI agents with HTML and JavaScript.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now