Vue with TypeScript

Vue · Advanced · 13 min

Most production Vue code is TypeScript; knowing the typed APIs prevents a lot of runtime bugs.

Vue 3 is written in TypeScript and <script setup lang="ts"> gives first-class typing. Type checking of templates is done by `vue-tsc` (run in CI with vue-tsc --noEmit) and the Vue language tools in your editor.

The main typed APIs:

  • Props: defineProps<{ title: string; size?: 'sm' | 'lg' }>(). With 3.5 destructuring, defaults are plain JavaScript: const { size = 'sm' } = defineProps<...>().
  • Emits: defineEmits<{ select: [id: number]; close: [] }>() types each event's arguments.
  • Models: defineModel<string>() or defineModel<number>({ required: true }).
  • Refs: inference usually works; annotate when the initial value is narrower than later values: ref<User | null>(null).
  • Template refs: useTemplateRef<HTMLInputElement>('input'); recent tooling infers it from the template automatically.
  • Provide/inject: InjectionKey<T> keys make inject return T | undefined.

Generic components take a type parameter on the script tag: <script setup lang="ts" generic="T extends { id: string }">. Then defineProps<{ items: T[] }>() and slot or emit types can reference T, and the parent's usage is checked against the inferred T.

Type-only imports in defineProps work for interfaces from other files. Keep prop types simple object shapes; the compiler needs to analyse them to generate runtime prop definitions.

Example

<script setup lang="ts" generic="T extends { id: number }">
import { ref, computed, useTemplateRef, onMounted } from 'vue'

const { items, max = 10 } = defineProps<{ items: T[]; max?: number }>()
const emit = defineEmits<{ select: [item: T]; clear: [] }>()
const query = defineModel<string>('query', { default: '' })

const selected = ref<T | null>(null)
const shown = computed(() => items.slice(0, max))
const search = useTemplateRef<HTMLInputElement>('search')
onMounted(() => search.value?.focus())

function pick(item: T) {
  selected.value = item
  emit('select', item)        // emit('select', 42) would be a type error
}
</script>

<template>
  <input ref="search" v-model="query">
  <slot v-for="item in shown" :key="item.id" :item="item" :pick="pick" />
</template>

Remember

  • defineProps<T>(), defineEmits<{ e: [args] }>(), defineModel<T>()
  • Annotate refs whose type widens: ref<User | null>(null)
  • generic="T" on the script tag creates generic components
  • Run vue-tsc --noEmit to type-check templates

Watch out for

  • Relying on tsc alone; it ignores .vue files and templates, so use vue-tsc.

Check yourself

  1. Which command type-checks .vue files including their templates?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: vuejs.org/guide/typescript/composition-api.html