Telerik blogs

These are the concepts you need to know when starting your Vue 3 + TypeScript journey.

Vue 3 is leaps ahead of its predecessor in ease of use and compatibility with TypeScript. However, sometimes the information on the key things to know and get started can be a little hard to digest.

In this article, I aim to explain and list the concepts that I feel are the most common and relevant to keep in mind when starting your Vue 3 + TS journey. Please note that this is not a TypeScript tutorial and I will assume basic knowledge of the language.

Tooling

TypeScript tooling and set up is quite a rabbit hole and one could probably write not one, but several in-depth articles about how to set up a project for the perfect integration between the two.

However, I think there is one important gotcha that I want to mention before we get going with actual TS and that is Volar’s takeover mode.

I assume you will be using Volar and VS Code here. If you are using different tooling, you can safely skip to the next section.

Save yourself some headaches now and go through the link above for the documentation on how to set up takeover mode—it’s not obvious that one has to set up VS Code like this for Volar to work properly with TypeScript.

Once you’re done, also make sure to check that the TypeScript version that Volar is using is the same as your package. This mismatch can quickly create some headaches when running type checks in CI or outside of your personal dev environment.

You’ll want to select “Use Workspace Version” unless you are 100% sure you want a different version powering Volar than what your workspace is using.

Working with Options API

When working with components created with the Options API, you will want to import and use the defineComponent helper from Vue to make sure that the component is correctly typed when imported into other files.

<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
  
})
</script>

You don’t need this when using script setup sugar, as it will already be typed correctly for you.

Typing Props

Correctly typing your component props is arguably one of the most important parts of using TS with Vue 3. This will ensure type safety even within your template tags, so how do we go about typing things?

<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
  props: {
    someNumber: { type: Number, default: 0 },
    user: { type: Object, default: 0 }
  }
})
</script>

<script setup lang="ts">
defineProps({
  someNumber: { type: Number, default: 0 },
  user: { type: Object, default: 0 }
})
</script>

In the above example, we have both Options API and Composition API defining the exact same props without any special kind of TS types. They both create a someNumber prop which is typed to a Number and a user typed to a generic Object.

When dealing with primitives like number, boolean, string, we don’t really need to do anything to tell TypeScript the type of the prop as it will be inferred by the type that Vue provides, like in the case of someNumber.

However, we probably want to use a more specific type for our user prop.

I will assume we have the following type defined:

interface User {
  id: number
  name: string
}

Now, we can use the special type PropType that Vue provides to more specifically define what that Object in user is.

<script lang="ts">
import { defineComponent, PropType } from 'vue'
export default defineComponent({
  props: {
    someNumber: { type: Number, default: 0 },
    user: { type: Object as PropType<User>, default: 0 }
  }
})
</script>

<script setup lang="ts">
import { PropType } from 'vue'
defineProps({
  someNumber: { type: Number, default: 0 },
  user: { type: Object as PropType<User>, default: 0 }
})
</script>

Typing Emits

Now that we have props correctly typed, we want to make sure that our emits are also strictly typed so that Volar and TypeScript can help us check that we are using emit payloads correctly.

<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
  emits: ['userSelected', 'click']
})
</script>

<script setup lang="ts">
defineEmits(['userSelected', 'click'])
</script>

In the above example, we see again both Options API and Composition API examples of how to define emits in a component. We are choosing to use the shorthand version of emits (there is a longer version that allows for validating the output similar to how props work).

The problem with leaving it like this when using TypeScript is that the content of the emit will be assumed to be any, which is not very helpful. Let’s go ahead and type these emits correctly.

We will assume that the click event is not emitting a payload, and the userSelected will pass a user object.

<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
  emits: {
    // eslint-disable-next-line
    'userSelected': (user: User) => true,
    // eslint-disable-next-line
    'click': () => true
  }
})
</script>

<script setup lang="ts">
defineEmits<{
  'userSelected': [user: User]
  'click': []
}>()
</script>

There’s a couple things worth unwrapping in this example. Let’s start with the Options API.

The emits property is now an object, instead of an array of strings. This is the extended way of defining emits, where each emit declares a function that works as a validation, similar to the validator property in props. I’ve opted to return true because we don’t need to validate the emit, since TypeScript is probably validation enough—this is up to you entirely.

The parameter found inside the validation function will be assumed by TypeScript to be the payload of the emit, so now when we have a component listening for the userSelected event, TypeScript will know we are expecting a User.

Note that I’ve added a disable on ESLint on both lines. Depending on your ESLint rules, this may not be necessary, but with the default Vue recommended rules you will get an error on the function since the user param is not being used. We don’t need it for click technically, but it’s become a habit of mine to add it everywhere to avoid headaches.

The Composition API version is also a bit different. Notice that the actual declaration of the emits is no longer a param inside the defineEmits function, but rather a type variable. So now it lives within the <{}> before the ().

We define our emits by setting a property like userSelected and then with a tuple syntax we determine the payload. So that [user: User] means we will have a user payload with a User type. Once again this will allow TypeScript to determine the payload that we may expect in another component when listening to this particular event.

Noteworthy Types

A few types that you are going to be using a lot (you import these out of 'vue') are:

Ref

A ref, like when creating a composition API ref value as in:

 

const user = ref({ id: 123, name: 'Marina' })

In cases like this, you may want to strictly type your ref, so that instead of TS assuming this is an object with an id and name, it’s a User type.

const user: Ref<User> = ref({ id: 123, name: 'Marina' })

Note that Ref can also accept ComputedRef types, such that if you have a function where you would expect either a ref or a computed value you can safely use Ref.

const myFn = (param: Ref) => { 
    return param.value // .value is defined as it exists in both computed and ref 
}

MaybeRef

If you are writing a lot of composables, you may find yourself using helper functions like unref a lot since we don’t know if our user is going to pass in a raw value or a ref. In these cases we can use the MaybeRef type.

 

export default (val: MaybeRef<boolean>) => {
   const rawBool = unref(val)
}

ComponentPublicInstance

When typing a template ref, you will want to use either ComponentPublicInstance to type it as any generic component.

 

<template>
  <p ref="myP">Example</p>
  <SomeComponent ref="myComp" />
</template>

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

const myP: Ref<HTMLElement|null> = ref(null)
const myComp: Ref<ComponentPublicInstance|null> = ref(null)

const doSomething = () => {
  // TS knows about $el because its a ComponentPublicInstance
  myComp.value?.$el
}
</script>

If you need to access specific methods within the component ref, however, you will have to use InstanceType to define it instead of ComponentPublicInstance.

<template>
  <p ref="myP">Example</p>
  <SomeComponent ref="myComp" />
</template>

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

const myP: Ref<HTMLElement|null> = ref(null)
const myComp: Ref<InstanceType<typeof SomeComponent>|null> = ref(null)

const doSomething = () => {
  // TS knows about myMethod inside of `SomeComponent`
  myComp.value?.myMethod()
}
</script>

Declaring Global Components

When using global components that are not specifically imported into our components, we need to declare them somewhere for TypeScript to work.

Within global.d.ts you can add the following declaration with your global components as you need. I’ve added an example with RouterLink and RouterView from Vue Router as they are commonly used as global components.

import BaseCheckbox from './globals/BaseCheckbox.vue'

declare module '@vue/runtime-core' {
  export interface GlobalComponents {
    RouterLink: typeof import('vue-router')['RouterLink']
    RouterView: typeof import('vue-router')['RouterView']

    // Custom components example
    BaseCheckbox: typeof BaseCheckbox
  }
}

Wrapping Up

This is really only scratching the surface of TS integration with Vue 3, but I hope that these must-know key topics help you get started quickly and effectively in your TS-Vue 3 journey.


Vue
About the Author

Marina Mosti

Marina Mosti is a frontend web developer with over 18 years of experience in the field. She enjoys mentoring other women on JavaScript and her favorite framework, Vue, as well as writing articles and tutorials for the community. In her spare time, she enjoys playing bass, drums and video games.

Related Posts

Comments

Comments are disabled in preview mode.