Vue and TypeScript

This section documents the current Vue 3.x release line as published at the official Vue.js documentation, which is the reference these pages are written and verified against. No specific patch version is pinned. The Composition API with <script setup> is the authoring style used throughout; the Options API is shown only as a contrast.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, since Vue and its ecosystem iterate quickly.

This section’s bibliography lists the reference material consulted while preparing these pages.

Vue is written in TypeScript and ships its own type definitions, so a typed Vue project needs very little configuration. This page covers only the Vue-specific typing patterns; for the language itself — the type system, generics, tsconfig, and the standard utility types — see the TypeScript Reference. It follows the official Using Vue with TypeScript overview.

Project setup

Scaffold with create-vue and pick the TypeScript option:

npm create vue@latest
# select "Add TypeScript?" -> Yes

The generated project type-checks with vue-tsc, a thin wrapper around tsc that also understands .vue single-file components. vite build only transpiles; run the checker separately (the template wires this into npm run build):

{
  "scripts": {
    "build": "vue-tsc --build && vite build",
    "type-check": "vue-tsc --build --watch"
  }
}

The template’s tsconfig splits config across tsconfig.app.json (app code) and tsconfig.node.json (Vite config, scripts). The app config pulls in Vite’s ambient client types so import.meta.env, *.svg?url, and friends are typed:

{
  "compilerOptions": {
    "types": ["vite/client"]
  }
}

create-vue also emits an env.d.ts with a .vue module shim so plain tsc-based editors resolve component imports:

/// <reference types="vite/client" />

declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}

With the official Vue (Volar) extension and "vueCompilerOptions": \{ "strictTemplates": true } in tsconfig, template expressions, prop bindings, and slot props are checked too.

Composition API typing

Add lang="ts" to <script setup> and most things infer. See TypeScript with Composition API.

ref, reactive, computed

ref(0) infers Ref<number>. Pass a type argument when the initial value is narrower than the eventual value, or when it starts null:

import { ref, reactive, computed, type Ref } from 'vue'

const count = ref(0)                     // Ref<number>
const user = ref<User | null>(null)      // needs the annotation
const ids: Ref<number[]> = ref([])

const state = reactive<{ open: boolean; items: string[] }>({ open: false, items: [] })

const double = computed(() => count.value * 2)          // ComputedRef<number>
const label = computed<string>(() => `#${count.value}`) // annotate a writable/complex computed

Avoid reactive’s generic with an interface that has optional properties — prefer `ref there. The Reactivity API reference lists every signature.

Type-based defineProps and defineEmits

Pass the shape as a type argument instead of a runtime object. The compiler generates the runtime declaration from it:

<script setup lang="ts">
interface Props {
  title: string
  count?: number
  tags: string[]
}
const props = defineProps<Props>()

const emit = defineEmits<{
  change: [id: number]          // tuple = positional payload
  submit: [value: string, silent: boolean]
}>()

emit('change', props.count ?? 0)
</script>

Defaults come from reactive props destructure (stable since Vue 3.5) — just destructure with =:

<script setup lang="ts">
const { count = 0, tags = [] } = defineProps<{ count?: number; tags?: string[] }>()
</script>

For older code, withDefaults wraps the call:

const props = withDefaults(defineProps<Props>(), {
  count: 0,
  tags: () => [],
})

defineModel

defineModel declares a two-way binding; the type argument fixes the value type and modifiers are typed via a second argument:

const model = defineModel<string>()                       // ModelRef<string | undefined>
const count = defineModel<number>('count', { default: 0 }) // ModelRef<number>
const [name, mods] = defineModel<string, 'trim' | 'lazy'>('name')

Template refs

useTemplateRef takes the element or component type as its argument:

<script setup lang="ts">
import { useTemplateRef, onMounted } from 'vue'
import Child from './Child.vue'

const input = useTemplateRef<HTMLInputElement>('input')
const child = useTemplateRef<InstanceType<typeof Child>>('child')

onMounted(() => input.value?.focus())
</script>

<template>
  <input ref="input" />
  <Child ref="child" />
</template>

Expose a typed public API from the child with defineExpose(\{ …​ }); InstanceType<typeof Child> then carries those members.

provide / inject

Carry the value type across the untyped provide/inject boundary with an InjectionKey<T>:

import { provide, inject, type InjectionKey, type Ref } from 'vue'

export const themeKey = Symbol() as InjectionKey<Ref<'light' | 'dark'>>

// provider
provide(themeKey, ref('light'))

// consumer -- inject(themeKey) is Ref<'light' | 'dark'> | undefined
const theme = inject(themeKey)
const themeOr = inject(themeKey, ref('light'))   // with default -> no undefined

Event handlers

Annotate the parameter of an inline handler, since $event is otherwise any. Cast event.target — the DOM types it as EventTarget | null:

<script setup lang="ts">
function onInput(event: Event) {
  const value = (event.target as HTMLInputElement).value
}
</script>

<template>
  <input @input="onInput" />
  <button @click="(e: MouseEvent) => console.log(e.clientX)">x</button>
</template>

Generic components

<script setup> accepts a generic attribute; the parameter is in scope for defineProps and emits:

<script setup lang="ts" generic="T extends { id: number }">
defineProps<{
  items: T[]
  selected: T | null
}>()
const emit = defineEmits<{ select: [item: T] }>()
</script>

See Generics for constraints and multiple parameters.

Options API typing

Wrap the component in defineComponent so this, props, and computed properties are inferred. This is the one place the official docs lean on the Options API, so it is shown here as a labelled contrast:

import { defineComponent, type PropType } from 'vue'

interface Book { title: string; year: number }

export default defineComponent({
  props: {
    books: { type: Array as PropType<Book[]>, required: true },
    mode: { type: String as PropType<'grid' | 'list'>, default: 'list' },
  },
  data() {
    return { query: '' }
  },
  computed: {
    // `this` is fully typed: this.books, this.mode, this.query
    filtered(): Book[] {
      return this.books.filter(b => b.title.includes(this.query))
    },
  },
})

PropType<T> is the bridge between a runtime constructor (Array, Object, Function) and the precise type you want. Add app-wide instance properties by augmenting ComponentCustomProperties:

// globals.d.ts
import 'vue'

declare module 'vue' {
  interface ComponentCustomProperties {
    $translate: (key: string) => string
  }
}

The same technique with ComponentCustomOptions types a custom component option consumed by a plugin.

Vue’s utility types

Vue exports helper types for working with prop definitions and ref-or-value parameters. The full list is in the Utility Types reference.

Type Purpose

PropType<T>

annotate the type field of a runtime prop as Object as PropType<T>

ExtractPropTypes<T>

derive the resolved props object (defaults applied, optionals narrowed) from a prop-options object

ExtractPublicPropTypes<T>

the same, but the public shape a parent passes (optionals stay optional)

ComponentCustomProperties

augmentation target for this.$x globals

ComponentCustomOptions

augmentation target for custom component options

MaybeRef<T>

T | Ref<T> — accept either a plain value or a ref (unwrap with unref / toValue)

MaybeRefOrGetter<T>

T | Ref<T> | (() ⇒ T) — the input type toValue normalises

CSSProperties

typed object for :style bindings

import { toValue, type MaybeRefOrGetter, type ExtractPropTypes } from 'vue'

function useTitle(source: MaybeRefOrGetter<string>) {
  watchEffect(() => { document.title = toValue(source) })
}

const listProps = {
  rows: { type: Array as PropType<string[]>, default: () => [] },
  dense: Boolean,
}
type ListProps = ExtractPropTypes<typeof listProps> // { rows: string[]; dense: boolean }

See also