Vue docs, chapter by chapter

TypeScript with Vue

The TypeScript chapters — project setup and tsconfig, typing props, emits, refs, computed, event handlers, provide/inject and template refs with the Composition API, the Options API equivalents, and the utility types.

Vue's TypeScript story is compiler-driven: the SFC compiler reads your type annotations and generates runtime declarations from them. The chapters list exactly which annotation goes where, and the interview questions follow: "how do you type emits", "how do you type a template ref to a component", "why can't reactive take a generic".

Know — Overview

  • Setup: create-vue scaffolds a typed project; vue-tsc type-checks SFCs on the command line (tsc cannot read .vue files); the Vue (Official) VS Code extension provides IDE support. Base configs come from @vue/tsconfig.
  • tsconfig: isolatedModules: true (each file transpiled alone, as Vite/esbuild do), verbatimModuleSyntax: true (explicit import type), strict: true, moduleResolution: 'bundler'; include .vue files through env.d.ts shims when needed.
  • defineComponent() enables type inference for Options API components and for setup() props; <script setup lang="ts"> needs no wrapper.
  • TS in templates: expressions may use TypeScript syntax such as (x as string).toUpperCase() when lang="ts" is set; the compiler strips it.
  • Generic components: <script setup lang="ts" generic="T extends string | number, U extends Item">; the generic names are usable in defineProps, defineEmits, defineSlots and can reference imported types.

Know — Composition API

  • Props: defineProps<{ foo: string, bar?: number }>(); the type argument must be a type literal or a reference to an interface/type alias in the same file, or (3.3+) an imported type with some limitations; defaults via withDefaults(defineProps<Props>(), { bar: 0 }) or reactive props destructure (3.5+); runtime-declared complex types use PropType<Book> (type: Object as PropType<Book>).
  • Emits: defineEmits<{ (e: 'change', id: number): void }>() (call signature) or the 3.3+ named tuple syntax defineEmits<{ change: [id: number] }>().
  • ref<T>(): ref<string | number>('2020'); ref<number>() without a value is Ref<number | undefined>. reactive is annotated with an interface, not a generic argument, because the returned type differs from the argument through nested ref unwrapping. computed<number>(() => …) when inference needs help.
  • Event handlers: annotate the parameter ((event: Event)) and cast the target ((event.target as HTMLInputElement).value).
  • Provide/inject: const key = Symbol() as InjectionKey<string> types both sides; a string key needs inject<string>('foo') (returns string | undefined) or inject('foo') as string when you know it exists.
  • Template refs: useTemplateRef<HTMLInputElement>('el'); in 3.5 with the Vue extension, the type is inferred automatically from the matching ref attribute. Component refs: InstanceType<typeof MyModal>; for generic or dynamic components use ComponentExposed<typeof MyGenericModal> from vue-component-type-helpers.

Know — Options API

  • Wrap in defineComponent. Props: type: Object as PropType<Book>, required: true for non-optional; arrow functions for validator/default lose this typing, so use method shorthand.
  • Emits with the object syntax and a typed validator; computed with an explicit return type when inference is circular; event handlers annotated as above.
  • Augmenting global properties: declare module 'vue' { interface ComponentCustomProperties { $http: typeof axios } } in a .ts file that is a module (add export {}). Custom options (beforeRouteEnter) via ComponentCustomOptions.

Know — Utility types

  • PropType<T> (runtime prop declarations), MaybeRef<T>, MaybeRefOrGetter<T> (composable inputs with toValue), ExtractPropTypes<T> (resolved props, defaults applied) and ExtractPublicPropTypes<T> (what the parent may pass, optional props stay optional), ComponentCustomProperties, ComponentCustomOptions, ComponentCustomProps, CSSProperties.

How it works

app/components/Docs/TypedForm.vue
<script setup lang="ts" generic="T extends { id: string }">
import type { InjectionKey } from 'vue'
import type { ComponentExposed } from 'vue-component-type-helpers'
import GenericModal from './GenericModal.vue'

// Props from a type literal, default via reactive destructure (3.5+)
const { items, dense = false } = defineProps<{ items: T[], dense?: boolean }>()

// Named tuple emits (3.3+)
const emit = defineEmits<{ select: [item: T], close: [] }>()

// Typed slots against the same generic
defineSlots<{ row(props: { item: T, index: number }): any }>()

// Refs: explicit generic; without initial value the type includes undefined
const selected = ref<T>()
const query = ref<string>('')

// reactive: annotate with an interface, not a generic argument
interface FormState { touched: boolean, errors: string[] }
const form: FormState = reactive({ touched: false, errors: [] })

// Provide/inject with a typed key
const CONFIG: InjectionKey<{ dense: boolean }> = Symbol('config')
provide(CONFIG, { dense })

// Template refs: element and generic component
const input = useTemplateRef<HTMLInputElement>('input')
const modal = useTemplateRef<ComponentExposed<typeof GenericModal>>('modal')

function onInput(event: Event) {
  query.value = (event.target as HTMLInputElement).value
}
</script>

<template>
  <input ref="input" :value="query" @input="onInput">
  <ul>
    <li v-for="(item, index) in items" :key="item.id" @click="selected = item; emit('select', item)">
      <slot name="row" :item :index>{{ item.id }}</slot>
    </li>
  </ul>
  <GenericModal ref="modal" @close="emit('close')" />
</template>

Options API augmentation, the part everyone has to look up:

types/global-properties.d.ts
import type { AxiosInstance } from 'axios'

declare module 'vue' {
  interface ComponentCustomProperties {
    $http: AxiosInstance
  }
}

export {} // makes this file a module so the augmentation applies
Gotcha· `reactive<T>()` is not the type you think

reactive<{ count: Ref<number> }>({ count: ref(0) }) fails because the returned type unwraps nested refs (count becomes number). Annotate the variable with an interface describing the unwrapped shape and let reactive infer.

Verify before the interview:

The current recommendation for typing template refs to generic components (ComponentExposed from vue-component-type-helpers) and whether the Vue extension now infers useTemplateRef types without the generic argument.

docs ↗

Exercise

Exercise
  • Convert a component's defineEmits from the call-signature syntax to named tuples and confirm both produce the same errors on a wrong payload.
  • Type a template ref to a generic component with ComponentExposed and call an exposed method with the wrong argument type.
  • Add a $http global property with the augmentation above, then remove export {} and observe how the augmentation breaks.

Be able to say

Be able to say· How do you type props, emits and refs in a Vue component?

"Props with a type argument to defineProps, defaults through withDefaults or reactive destructure, and PropType only when I declare props at runtime with complex types. Emits with the named tuple syntax, defineEmits<{ change: [id: number] }>(), which reads as a contract. Refs with an explicit generic when the initial value does not tell the whole story, remembering that ref<T>() without a value includes undefined, and reactive with an interface annotation on the variable because its return type unwraps nested refs. Template refs with useTemplateRef<HTMLInputElement>, and component refs with InstanceType<typeof Comp>, or ComponentExposed from the type helpers when the component is generic."

Your notes

Open in Notes

Nothing yet. Notes filed here show up on this page and the section overview.