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>()ordefineModel<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 makeinjectreturnT | 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 --noEmitto type-check templates
Watch out for
- Relying on
tscalone; it ignores.vuefiles and templates, so usevue-tsc.