Vue.js 3 TypeScript Setup & Basics Tutorial

In this Vue tutorial we learn how to use TypeScript in Vue instead of Javascript.

We cover how to add TypeScript support, defining components, type inference and explicit typing computed properties and props.

Lesson Project

We can set up TypeScript in Vue in multiple ways.

  • Add it to an existing project with vue add typescript. We cover this process at the end of the lesson .
  • Generate a new project and select TypeScript support.

To keep things simple, we’ll generate a new project with the following choices.

We want to Manually select features

Example:
? Please pick a preset:
  Default ([Vue 2] babel, eslint)
  Default (Vue 3) ([Vue 3] babel, eslint)
> Manually select features

Choose TypeScript

Example:
? Please pick a preset: Manually select features
? Check the features needed for your project:
 (*) Choose Vue version
 (*) Babel
>(*) TypeScript
 ( ) Progressive Web App (PWA) Support
 ( ) Router
 ( ) Vuex
 ( ) CSS Pre-processors
 (*) Linter / Formatter
 ( ) Unit Testing
 ( ) E2E Testing

To keep things simple, we’ll choose n for class-style component syntax.

Example:
? Use class-style component syntax? (y/N) n

We’ll add Babel for transpiling etc. with y.

Example:
? Use Babel alongside TypeScript? (Y/n) y

tip If you’re using VSCode as your editor, you can cd into your new project and run the following command to open the project in the editor.

Command:
# change directory
# into 'projectname'
cd projectname

# open current directory
# in visual studio code
code .

Editors

If you’re developing Vue applications with TypeScript, it’s recommended to use either Visual Studio Code (free) or Webstorm (commercial) editors. Both editors have integrated support for TypeScript.

Visual Studio Code Plugin

If you’re using VSCode as your editor, we recommend using the free Vetur or Volar extension, which provides additional features like TypeScript inference inside components.

What is TypeScript?

TypeScript is a strongly typed superset of ES6+ Javascript. It’s designed by Microsoft for large-scale Javascript application development and compiles back to plain ES6+ Javascript.

TypeScript provides us with multiple benefits over native Javascript.

  • TypeScript is simple, fast, and easy to learn.
  • TypeScript supports all Javascript libraries.
  • TypeScript is a safer approach to JavaScript.
  • TypeScript is portable, it can run on any environment Javascript runs on. It doesn’t need a VM or specific runtime environment.
  • TypeScript is statically typed, therefore code written in TypeScript is more predictable, and is generally easier to debug.
  • TypeScript supports OOP features like classes, inheritance, interfaces, generics etc.
  • TypeScript provides compile time error-checking.
  • TypeScript tooling provides autocompletion, type checking and source code documentation.

tip If you’re new to TypeScript, we recommend learning it first with our introductory course before continuing with the following lessons.

Project Files

Most of the files in your project will look the same, but there are a few changes we should address with the following files.

Example: project
project/
├── src/
|   ├── App.vue
|   ├── main.ts
|   └── shims-vue.d.ts
└── tsconfig.ts

Overview:

  • The main.ts file is just the TypeScript version of the main.js file.
  • The shims-vue.d.ts file tells TypeScript how to understand single file Vue components. You won’t need to do anything in this file.
  • The tsconfig.ts file contains the configuration options for the TypeScript compiler.
  • We will dicuss the changes in the App.vue file in more depth in the section below.

Development language and compiling

We use the lang attribute with a value of “ts” on the script block to tell Vue that we’re using TypeScript as the language.

Syntax: lang="ts"
<script lang="ts">

</script>

We don’t have to worry about compiling TypeScript down to Javascript, Vue does it for us in its compilation step. We can compile our application as we normally would with one of the commands in the “script” section of the package.json file.

Command:
# spin up a dev server
# and watch for changes
npm run serve

# build the app for production
npm run build

# run a linter on the code
npm run lint

How to define a component with TypeScript

To define a component with TypeScript, we use the defineComponent method, imported from the ‘vue’ package.

The method takes a single argument.

  • The object configuration that represents the component.
Syntax: defineComponent
<script lang="ts">
import { defineComponent } from 'vue';

export default defineComponent({
  // config
});
</script>

As an example, let’s define a simple component that outputs a data property to the template.

Example: src/App.vue
<template>
  <h1>{{ msg }}</h1>
</template>

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

export default defineComponent({
  data() {
    return { msg: 'Hello, World!' }
  }
});
</script>

The defineComponent method lets TypeScript properly infer types inside the data option.

Type inference in the data option

In TypeScript, we explicitly define a type after the name with the colon operator separating them.

Example: typing
let name:string = 'John'

We can also use multiple types by separating them with a | (pipe) operator.

Example: union typing
let num: string | number = 25

Type inference means that TypeScript will automatically type the data property based on its value.

As an example, let’s create a method that changes the data property to a new value. We’ll type the new value as a number.

Example: src/App.vue
<template>
  <h1>{{ msg }}</h1>
  <button @click="changeValue(20)">Change value</button>
</template>

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

export default defineComponent({
  data() {
    return { msg: 'Hello, World!' }
  },
  methods: {
    changeValue(newVal:number) {
      this.msg = newVal;
    }
  }
});
</script>

If we try to save the example, the compiler raises an error.

Output:
Type 'number' is not assignable to type 'string'.

Even though we didn’t explicitly define a type for the “msg” data property, TypeScript inferred it from its string value.

If we wanted msg to be able to accept another type of value, we would need to explicitly define one or more types for it.

Explicit typing in the data option

Because everything inside our data option is a property and not a variable, we have to use type assertion (type casting) with the as keyword to explicitly define its type.

Syntax:
data() {
  return {
    property: value as type
  }
}

As an example, let’s explicitly type our msg property as a number and a string to match the input from the method.

Example: src/App.vue
<template>
  <h1>{{ msg }}</h1>
  <button @click="changeValue(20)">Change value</button>
</template>

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

export default defineComponent({
  data() {
    return {
      msg: 'Hello, World!' as number | string
    }
  },
  methods: {
    changeValue(newVal:number | string) {
      this.msg = newVal;
    }
  }
});
</script>

This time, the app will compile and when we click on the button in the browser, it will change the value to 20.

Complex types or interfaces

When we’re working with a more complex type, like an interface , we define the types we want in the interface and then cast the data property as that interface.

Syntax: interface
// define the complex type
interface TypeName {
  propertyName: type
}

// cast to the interface
data() {
  return {
    propertyObject: {
      // match interface property
      propertyName: value

    // cast as interface
    } as TypeName
  }
}

As an example, let’s create a Person interface with first and last name properties as strings. In the data option, we’ll create an object with the same properties and cast it as the interface.

Example: src/App.vue
<template>
  <p>Hello {{ p.firstName }} {{ p.lastName }}</p>
</template>

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

interface Person {
  firstName: string,
  lastName: string
}

export default defineComponent({
  data() {
    return {
      p: {
        firstName: 'John',
        lastName: 'Doe'

      } as Person
    }
  }
})
</script>

When we run the example in the browser, it will show the first and last names. But now we have type checking.

To demonstrate, let’s change the last name from a string value to a number.

Example: src/App.vue
<template>
  <p>Hello {{ p.firstName }} {{ p.lastName }}</p>
</template>

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

interface Person {
  firstName: string,
  lastName: string
}

export default defineComponent({
  data() {
    return {
      p: {
        firstName: 'John',
        lastName: 20

      } as Person
    }
  }
})
</script>

If we try to save the file, the compiler will raise the following error.

Output:
Conversion of type '{ firstName: string; lastName: number; }' to
type 'Person' may be a mistake because neither type sufficiently
overlaps with the other.

If this was intentional, convert the expression to 'unknown' first.

  Types of property 'lastName' are incompatible.
    Type 'number' is not comparable to type 'string'.

It works the same when the type is defined in an external file. All we need to do is import it to be able to cast to it.

To demonstrate, let’s create a new folder with a standard TypeScript file.

  • src/types/Person.ts

The project should look similar to the following.

Example: project
project-name/
├── src/
|   ├── types/
|   |   └── Person.ts
|   └── App.vue

The Person.ts file will contain the exported Person interface from the previous example.

Example: src/types/Person.ts
export default interface Person {
  firstName: string,
  lastName: string
}

We’ll import it into the root App component for the cast in the data option. We’ll also change the last name back to a string value.

Example: src/App.vue
<template>
  <p>Hello {{ p.firstName }} {{ p.lastName }}</p>
</template>

<script lang="ts">
import { defineComponent } from "vue"
// import type
import Person from '@/types/Person'

export default defineComponent({
  data() {
    return {
      p: {
        firstName: 'John',
        lastName: 'Doe'

      } as Person
    }
  }
})
</script>

When we run the example in the browser, everything works as expected.

Function return value type inference

TypeScript can infer the return value of a function, just like it does with data properties.

So far in our examples, we didn’t specify a return type and everything worked correctly.

Example:
methods: {
  changeValue(newVal:number | string) {
    this.msg = newVal;
  }
}

However, it’s considered good practice in TypeScript to explicitly type a return value from a function after its parameter list.

Syntax:
// single type
functionName(): returnType {}

// multiple types
functionName(): returnType1 | returnType2 {}

As an example, let’s add a number and string return type to our function and return the property assignment.

Example: src/App.vue
<template>
  <h1>{{ msg }}</h1>
  <button @click="changeValue(20)">Change value</button>
</template>

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

export default defineComponent({
  data() {
    return {
      msg: 'Hello, World!' as number | string
    }
  },
  methods: {
    changeValue(newVal:number | string): number | string {
      // a typed return value
      // must return something
      return this.msg = newVal;
    }
  }
})
</script>

If we run the example in the browser and click on the button, everything still works as expected.

note In this case we’re not using the return value, but a function with an explicit return type must return a value.

Explicit typing a computed property

Sometimes Vue has trouble automatically inferring computed property types.

As an example, let’s create a computed property that combines a first and last name into a fullname.

Example: src/App.vue
<template>
  <p>Hello {{ fullName }}</p>
</template>

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

export default defineComponent({
  data() {
    return {
      firstName: 'John',
      lastName: 'Doe'
    }
  },
  computed: {
    fullName() {
      return `${this.firstName} ${this.lastName}`;
    }
  }
})
</script>

If we try to save the file, the compiler will raise an error similar to the following.

Output:
ERROR in src/App.vue:17:40
TS2339: Property 'firstName' does not exist on type 'ComponentPublicInstance<{}
TS2339: Property 'lastName' does not exist on type 'ComponentPublicInstance<{}

To fix the error, all we need to do is add a return type to the computed property.

Example: explicit return type
computed: {
  propertyName(): type {
    return value;
  }
}

Let’s add a return type to the computed property in our example. In this case, we’re returning a string .

Example: src/App.vue
<template>
  <p>Hello {{ fullName }}</p>
</template>

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

export default defineComponent({
  data() {
    return {
      firstName: 'John',
      lastName: 'Doe'
    }
  },
  computed: {
    // needs return type
    fullName(): string {
      return `${this.firstName} ${this.lastName}`;
    }
  }
})
</script>

This time the application compiles and renders the greeting message as expected.

tip This process is also known as annotation, or type hinting.

Explicit typing computed property getters & setters

When a computed property has getters and setters , the getter needs to have a return type.

Syntax: getter annotation
computed: {
  propertyName: {
    // needs return type
    get(): type {
      return value;
    },
    set(newValue: type) {
      this.value = newValue;
    }
  }
}

tip The setter doesn’t need to have a return type because we’re setting a value that already has a type.

To demonstrate, let’s change the computed property in our example to use a typed getter.

note We don’t care about an implementation for the setter in this example, so we can just let it log an empty string to the console.

Example: src/App.vue
<template>
  <p>Hello {{ fullName }}</p>
</template>

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

export default defineComponent({
  data() {
    return {
      firstName: 'John',
      lastName: 'Doe'
    }
  },
  computed: {
    fullName: {
      get(): string {
        return this.firstName + ' ' + this.lastName;
      },
      set() { console.log(''); }
    }
  }
})
</script>

When we run the example in the browser, everything still works as expected.

If we removed the type from the getter, the compiler would raise the same “Property does not exist on type” error as before.

Explicit typing props

Vue allows us to specify a soft prop type when we’re working with Javascript.

Syntax: soft prop type
props: {
  propName: type
}

If the prop’s type doesn’t match the one we defined, the application will still compile but raise a warning in the console.

To provide these types to TypeScript, we need to use type assertion (type casting) on a prop’s type property with Vue’s PropType generic , imported from the ‘vue’ package.

Syntax: PropType
props: {
  propName: {
    type: Type as PropType<type>
  }
}

As an example, let’s create a new component that will act as the child component we are sending the props to.

  • src/components/GreetingMessage.vue

The project should look similar to the following.

Example: project
project-name/
├── src/
|   ├── components/
|   |   └── GreetingMessage.vue
|   └── App.vue

The GreetingMessage component will accept two props as string and number types and output them in the template.

Example: src/components/GreetingMessage.vue
<template>
  <p>Hello, my name is {{ name }} and I am {{ age }}</p>
</template>

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

export default defineComponent({
  props: {
    name: {
      type: String as PropType<string>
    },
    age: {
      type: Number as PropType<number>
    }
  }
})
</script>

In the root App component, we’ll import and use the greeting component with the two props in the template.

note When importing components in TypeScript, we have to add the .vue extension to the statement.

Example: src/App.vue
<template>
  <greeting-message :name="'John'" :age="20" />
</template>

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

export default defineComponent({
  components: { GreetingMessage }
})
</script>

When we run the example in the browser, everything works as expected.

If we change the age to another type, like a string, Vue will raise a warning in the console.

Output:
Invalid prop: type check failed for prop "age".
  Expected Number with value 20, got String with value "20".

Complex types or interfaces

If we’re using more complex types like an interface, we cast an Object as the interface.

We also have to set the required option to either true or false.

Syntax: interface PropType
props: {
  propName: {
    type: Object as PropType<InterfaceName>,
    required: true/false
  }
}

As an example, we’ll use the Person interface from earlier in the lesson.

  • src/types/Person.ts

The project should look similar to the following.

Example: project
project-name/
├── src/
|   ├── types/
|   |   └── Person.ts
|   ├── components/
|   |   └── GreetingMessage.vue
|   └── App.vue

The interface will have the name and age properties with their types.

Syntax: src/types/Person.ts
export default interface Person {
  name: string,
  age: number
}

We’ll import the interface into the GreetingMessage component and cast a person prop as the interface.

Example: src/components/GreetingMessage.vue
<template>
  <p>Hello, my name is {{ person.name }} and I am {{ person.age }}</p>
</template>

<script lang="ts">
import { defineComponent, PropType } from "vue"
import Person from '@/types/Person'

export default defineComponent({
  props: {
    person: {
      type: Object as PropType<Person>,
      required: true,
    }
  }
})
</script>

In the root App component, we’ll bind to the new person prop on the component instance and pass an object to it with the individual property values.

Example: src/App.vue
<template>
  <greeting-message :person="{ name: 'Jane', age: 19 }" />
</template>

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

export default defineComponent({
  components: { GreetingMessage }
})
</script>

When we run the example in the browser, everything works as expected.

Statement termination convention

In Javascript, we are allowed to omit the ; (semicolon) statement termination operator because of ASI (Automatic Semicolon Insertion).

Example:
methods: {
  changeValue(newVal:number | string) {
    // no semicolon
    this.msg = newVal
  }
}

While TypeScript also use ASI, the convention is to use the semicolon to terminate statements.

Example:
methods: {
  changeValue(newVal:number | string) {
    // explicit semicolon
    this.msg = newVal;
  }
}

There are pro’s and cons for both styles, which we won’t go into here. Whichever one you choose, the important thing is to stay consistent throughout the project.

note In this course we don’t use the semicolon in the Javascript sections, but we will use the semicolon for this TypeScript section.

How to add TypeScript to an existing Vue project

Vue makes it easy to add TypeScript to an existing project by running the following command in your project’s directory.

tip If you’re working in VSCode, you can go to Terminal > New Terminal to open a terminal or command prompt with the project folder already selected.

Command: add typescript
vue add typescript

Vue will ask you to choose some settings.

1. Use class-style component syntax?

Class-style component syntax uses TypeScript classes and decorators to create components.

tip If you’ve used Angular before, this style will be familiar to you.

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

@Component
export default class HelloWorld extends Vue {

}
</script>

If you don’t want to use class-style component syntax, choose N.

2. Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)?

Babel converts modern TypeScript into backwards compatible versions so that it can run in both current and older browsers. It can transform syntax, add polyfills, etc.

We recommend using it so choose Y.

3. Convert all .js files to .ts?

Vue will do a good job of converting your Javascript to TypeScript so you can choose Y.

4. Allow .js files to be compiled?

If you’re new to TypeScript or Vue, it may be useful to allow Javascript files to be compiled so you can choose Y.

5. Skip type checking of all declaration files (recommended for apps)?

This option tells the compiler to skip type checking on all declaration ( *.d.ts ) files. This includes declaration files from the node_modules directory.

It’s recommended and has the added benefit of speeding up compilation time so you can choose Y.

Once the process is complete, you will see the following changes (among others) in your project.

  • A new tsconfig.json file in your root directory with the settings for TypeScript compiler.
  • All .js files have been converted into .ts files.
  • Components will use lang="ts" on their script blocks.
  • Components will be defined with the defineComponent method.

Further Reading

For more information on the topics covered in this lesson, please see the relevant sections below.