script setup with TypeScript

Intermediate
13 min

script setup with TypeScript

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.

Project Setup

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:

json
{ "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.

Typing Props

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:

vue
<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 }.

Typing Emits, Models and Slots

defineEmits accepts a type describing each event name and its payload as a labeled tuple:

vue
<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 }">.

Typing refs, computed and Template Refs

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:

typescript
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.

Generic Components

A list component that should preserve the item type for its scoped slot can declare a type parameter with the generic attribute:

vue
<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.

Common mistakes

  • Using lang="ts" on <script setup> and forgetting it on a separate <script> block; both must match.
  • Mixing the type argument and the runtime object in one defineProps call, which is not allowed.
  • Typing a ref as ref<Task[]>([]) and then assigning readonly data to it; use Readonly<Task[]> if the source is immutable.
Quick Quiz
Question 1 of 2

Which command performs full type checking of `.vue` files?

Key Takeaways

  • Use <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.
  • Give ref an explicit type when the initial value is null or a union; type template refs with the element type.
  • The 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.

script setup with TypeScript - Vue.js | CodeYourCraft | CodeYourCraft