Vue 3 was written in TypeScript and <script setup lang="ts"> gives you full type inference in props, emits, refs and templates. With the "Vue - Official" editor extension, a typo in a prop name or a wrong event payload is underlined before you save. After this lesson you will type every part of a component, run type checking in CI, and write generic components.
npm create vue@latest asks whether to add TypeScript; answer yes and it configures tsconfig.json, the vue-tsc type checker and typed env.d.ts. In an existing project, install typescript and vue-tsc, then add a script:
{
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"build-only": "vite build",
"type-check": "vue-tsc --build"
}
}Vite strips types without checking them, which keeps the dev server fast. vue-tsc performs the real check across .ts and .vue files, so run npm run type-check locally and in CI.
Pass a type argument to defineProps instead of a runtime object. Optional props use ?, and since Vue 3.5 you can destructure with defaults while keeping reactivity:
<script setup lang="ts">
interface Task {
id: number
title: string
done: boolean
}
const { tasks, title = 'Tasks', dense = false } = defineProps<{
tasks: Task[]
title?: string
dense?: boolean
}>()
</script>The compiler turns the destructured names into props.tasks and props.title accesses, so they stay reactive. Before 3.5 the same was written with withDefaults(defineProps<Props>(), { title: 'Tasks' }), which still works. Types can be imported from another file (import type { Task } from '@/types') and used directly in the generic argument.
Options API components written with defineComponent({ ... }) also get inference for data, computed and this; for complex prop types in the runtime object form, use PropType: items: { type: Array as PropType<Task[]>, required: true }.
defineEmits accepts a type describing each event name and its payload as a labeled tuple:
<script setup lang="ts">
const emit = defineEmits<{
toggle: [id: number]
rename: [id: number, title: string]
}>()
const model = defineModel<string>({ required: true })
const open = defineModel<boolean>('open', { default: false })
defineSlots<{
default(props: { task: Task }): any
empty(): any
}>()
</script>emit('toggle', '3') is now a compile error. defineModel<T>() types the value that v-model binds, and defineSlots documents slot props so consumers get completion inside <template #default="{ task }">.
Inference covers the common cases: ref(0) is Ref<number> and computed(() => tasks.length) is ComputedRef<number>. Provide an explicit type when the initial value is null or a union:
import { ref, computed, useTemplateRef } from 'vue'
const selected = ref<Task | null>(null)
const status = ref<'idle' | 'loading' | 'error'>('idle')
const input = useTemplateRef<HTMLInputElement>('input')
const total = computed<number>(() => tasks.reduce((n, t) => n + (t.done ? 1 : 0), 0))
function onInput(event: Event) {
const value = (event.target as HTMLInputElement).value
}Because input.value may be null before mount, TypeScript forces a check: input.value?.focus(). In provide/inject, use InjectionKey<T> as shown in the provide/inject lesson.
A list component that should preserve the item type for its scoped slot can declare a type parameter with the generic attribute:
<script setup lang="ts" generic="T extends { id: number }">
defineProps<{
items: T[]
selected?: T
}>()
defineEmits<{ select: [item: T] }>()
</script>
<template>
<li v-for="item in items" :key="item.id" @click="$emit('select', item)">
<slot :item="item" />
</li>
</template>When a parent passes :items="users", the select payload and the slot prop item are typed as User automatically.
lang="ts" on <script setup> and forgetting it on a separate <script> block; both must match.defineProps call, which is not allowed.ref<Task[]>([]) and then assigning readonly data to it; use Readonly<Task[]> if the source is immutable.Which command performs full type checking of `.vue` files?
<script setup lang="ts"> and run vue-tsc --build for type checking; Vite itself does not check types.defineProps<T>() with reactive destructuring (or withDefaults) types props and defaults.defineEmits<{ event: [payload] }>(), defineModel<T>() and defineSlots type the rest of the component contract.ref an explicit type when the initial value is null or a union; type template refs with the element type.generic attribute creates generic components whose slot and event types follow the input data.Next lesson: Routing with Vue Router — map URLs to components and navigate between pages in a single-page application.