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 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 |
|---|---|
|
annotate the |
|
derive the resolved props object (defaults applied, optionals narrowed) from a prop-options object |
|
the same, but the public shape a parent passes (optionals stay optional) |
|
augmentation target for |
|
augmentation target for custom component options |
|
|
|
|
|
typed object for |
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
-
TypeScript Reference — the language: structural typing, generics, utility types,
tsconfig. -
JavaScript Development — the JavaScript baseline underneath.
-
React Reference and Angular Reference — how the other frameworks approach typed components.