TypeScript with Composition API
Prerequisites
- Familiarity with Using Vue with TypeScript.
Typing Component Props
Section titled “Typing Component Props”Using <script setup>
Section titled “Using <script setup>”When using <script setup>
, the defineProps()
macro supports inferring the props types based on its argument:
<script setup lang="ts">const props = defineProps({ foo: { type: String, required: true }, bar: Number})
props.foo // stringprops.bar // number | undefined</script>
This is called “runtime declaration”, because the argument passed to defineProps()
will be used as the runtime props
option.
However, it is usually more straightforward to define props with pure types via a generic type argument:
<script setup lang="ts">const props = defineProps<{ foo: string bar?: number}>()</script>
This is called type-based declaration. The compiler will try to do its best to infer the equivalent runtime options based on the type argument. In this case, our second example compiles into the exact same runtime options as the first example.
You can use either type-based declaration OR runtime declaration, but you cannot use both at the same time.
You can also move the props types into a separate interface:
<script setup lang="ts">interface Props { foo: string bar?: number}
const props = defineProps<Props>()</script>
This also works if Props
is imported from an external source. This feature requires TypeScript to be a peer dependency of Vue.
<script setup lang="ts">import type { Props } from './foo'
const props = defineProps<Props>()</script>
Syntax Limitations
Section titled “Syntax Limitations”In version 3.2 and below, the generic type parameter for defineProps()
were limited to a type literal or a reference to a local interface.
This limitation has been resolved in 3.3. The latest version of Vue supports referencing imported and a limited set of complex types in the type parameter position. However, because the type to runtime conversion is still AST-based, some complex types that require actual type analysis, e.g. conditional types, are not supported. You can use conditional types for the type of a single prop, but not the entire props object.
Props Default Values
Section titled “Props Default Values”When using type-based declaration, you lose the ability to declare default values for the props. This can be resolved by using Reactive Props Destructure 3.5+ :
interface Props { msg?: string labels?: string[]}
const { msg = 'hello', labels = ['one', 'two'] } = defineProps<Props>()
In 3.4 and below, Reactive Props Destructure is not enabled by default. An alternative is to use the withDefaults
compiler macro:
interface Props { msg?: string labels?: string[]}
const props = withDefaults(defineProps<Props>(), { msg: 'hello', labels: () => ['one', 'two']})
This will be compiled to equivalent runtime props default
options. In addition, the withDefaults
helper provides type checks for the default values, and ensures the returned props
type has the optional flags removed for properties that do have default values declared.
Without <script setup>
Section titled “Without <script setup>”If not using <script setup>
, it is necessary to use defineComponent()
to enable props type inference. The type of the props object passed to setup()
is inferred from the props
option.
import { defineComponent } from 'vue'
export default defineComponent({ props: { message: String }, setup(props) { props.message // <-- type: string }})
Complex prop types
Section titled “Complex prop types”With type-based declaration, a prop can use a complex type much like any other type:
<script setup lang="ts">interface Book { title: string author: string year: number}
const props = defineProps<{ book: Book}>()</script>
For runtime declaration, you can use the PropType
utility type:
import type { PropType } from 'vue'
const props = defineProps({ book: Object as PropType<Book>})
This works in much the same way if you’re specifying the props option directly:
import { defineComponent } from 'vue'import type { PropType } from 'vue'
export default defineComponent({ props: { book: Object as PropType<Book> }})
The props
option is more commonly used with the Options API, so you’ll find more detailed examples in the guide to TypeScript with Options API. The techniques shown in those examples also apply to runtime declarations using defineProps()
.
Typing Component Emits
Section titled “Typing Component Emits”In <script setup>
, the emit
function can also be typed using either runtime declaration OR type declaration:
<script setup lang="ts">// runtimeconst emit = defineEmits(['change', 'update'])
// options basedconst emit = defineEmits({ change: (id: number) => { // return `true` or `false` to indicate // validation pass / fail }, update: (value: string) => { // return `true` or `false` to indicate // validation pass / fail }})
// type-basedconst emit = defineEmits<{ (e: 'change', id: number): void (e: 'update', value: string): void}>()
// 3.3+: alternative, more succinct syntaxconst emit = defineEmits<{ change: [id: number] update: [value: string]}>()</script>
The type argument can be one of the following:
- A callable function type, but written as a type literal with Call Signatures. It will be used as the type of the returned
emit
function. - A type literal where the keys are the event names, and values are array / tuple types representing the additional accepted parameters for the event. The example above is using named tuples so each argument can have an explicit name.
As you can see, the type declaration gives us much finer-grained control over the type constraints of emitted events.
When not using <script setup>
, defineComponent()
is able to infer the allowed events for the emit
function exposed on the setup context:
import { defineComponent } from 'vue'
export default defineComponent({ emits: ['change'], setup(props, { emit }) { emit('change') // <-- type check / auto-completion }})
Typing ref()
Section titled “Typing ref()”Refs infer the type from the initial value:
import { ref } from 'vue'
// inferred type: Ref<number>const year = ref(2020)
// => TS Error: Type 'string' is not assignable to type 'number'.year.value = '2020'
Sometimes you may need to specify complex types for a ref’s inner value. You can do that by using the Ref
type:
import { ref } from 'vue'import type { Ref } from 'vue'
const year: Ref<string | number> = ref('2020')
year.value = 2020 // ok!
Or, by passing a generic argument when calling ref()
to override the default inference:
// resulting type: Ref<string | number>const year = ref<string | number>('2020')
year.value = 2020 // ok!
If you specify a generic type argument but omit the initial value, the resulting type will be a union type that includes undefined
:
// inferred type: Ref<number | undefined>const n = ref<number>()
Typing reactive()
Section titled “Typing reactive()”reactive()
also infers the type from the initial value:
import { reactive } from 'vue'
// inferred type: { title: string }const book = reactive({ title: 'Vue 3 Guide' })
To explicitly type a reactive
property, you can use interfaces:
import { reactive } from 'vue'
interface Book { title: string year?: number}
const book: Book = reactive({ title: 'Vue 3 Guide' })
Typing computed()
Section titled “Typing computed()”computed()
infers its type based on the getter’s return value:
import { ref, computed } from 'vue'
const count = ref(0)
// inferred type: ComputedRef<number>const double = computed(() => count.value * 2)
// => TS Error: Property 'split' does not exist on type 'number'const result = double.value.split('')
You can also specify an explicit type via a generic argument:
const double = computed<number>(() => { // type error if this doesn't return a number})
Typing Event Handlers
Section titled “Typing Event Handlers”When dealing with native DOM events, it might be useful to type the argument we pass to the handler correctly. Let’s take a look at this example:
<script setup lang="ts">function handleChange(event) { // `event` implicitly has `any` type console.log(event.target.value)}</script>
<template> <input type="text" @change="handleChange" /></template>
Without type annotation, the event
argument will implicitly have a type of any
. This will also result in a TS error if "strict": true
or "noImplicitAny": true
are used in tsconfig.json
. It is therefore recommended to explicitly annotate the argument of event handlers. In addition, you may need to use type assertions when accessing the properties of event
:
function handleChange(event: Event) { console.log((event.target as HTMLInputElement).value)}
Typing Provide / Inject
Section titled “Typing Provide / Inject”Provide and inject are usually performed in separate components. To properly type injected values, Vue provides an InjectionKey
interface, which is a generic type that extends Symbol
. It can be used to sync the type of the injected value between the provider and the consumer:
import { provide, inject } from 'vue'import type { InjectionKey } from 'vue'
const key = Symbol() as InjectionKey<string>
provide(key, 'foo') // providing non-string value will result in error
const foo = inject(key) // type of foo: string | undefined
It’s recommended to place the injection key in a separate file so that it can be imported in multiple components.
When using string injection keys, the type of the injected value will be unknown
, and needs to be explicitly declared via a generic type argument:
const foo = inject<string>('foo') // type: string | undefined
If you are sure that the value is always provided, you can also force cast the value:
const foo = inject('foo') as string
Typing Template Refs
Section titled “Typing Template Refs”With Vue 3.5 and @vue/language-tools
2.1 (powering both the IDE language service and vue-tsc
), the type of refs created by useTemplateRef()
in SFCs can be automatically inferred for static refs based on what element the matching ref
attribute is used on.
In cases where auto-inference is not possible, you can still cast the template ref to an explicit type via the generic argument:
const el = useTemplateRef<HTMLInputElement>('el')
Template refs should be created with an explicit generic type argument and an initial value of null
:
<script setup lang="ts">import { ref, onMounted } from 'vue'
const el = ref<HTMLInputElement | null>(null)
onMounted(() => { el.value?.focus()})</script>
<template> <input ref="el" /></template>
To get the right DOM interface you can check pages like MDN.
Typing Component Template Refs
Section titled “Typing Component Template Refs”With Vue 3.5 and @vue/language-tools 2.1
(powering both the IDE language service and vue-tsc
), the type of refs created by useTemplateRef()
in SFCs can be automatically inferred for static refs based on what element or component the matching ref
attribute is used on.
In cases where auto-inference is not possible (e.g. non-SFC usage or dynamic components), you can still cast the template ref to an explicit type via the generic argument.
In order to get the instance type of an imported component, you need to:
- Get its type via
typeof
, - Use TypeScript’s built-in
InstanceType
utility to extract its instance type:
<script setup lang="ts">import { useTemplateRef } from 'vue'import Foo from './Foo.vue'import Bar from './Bar.vue'
type FooType = InstanceType<typeof Foo>type BarType = InstanceType<typeof Bar>
const compRef = useTemplateRef<FooType | BarType>('comp')</script>
<template> <component :is="Math.random() > 0.5 ? Foo : Bar" ref="comp" /></template>
In cases where the exact type of the component isn’t available or isn’t important, ComponentPublicInstance
can be used instead. This will only include properties that are shared by all components, such as $el
:
import { useTemplateRef } from 'vue'import type { ComponentPublicInstance } from 'vue'
const child = useTemplateRef<ComponentPublicInstance>('child')
In cases where the component referenced is a generic component, for instance MyGenericModal
:
<script setup lang="ts" generic="ContentType extends string | number">import { ref } from 'vue'
const content = ref<ContentType | null>(null)
const open = (newContent: ContentType) => (content.value = newContent)
defineExpose({ open})</script>
It needs to be referenced using ComponentExposed
from the vue-component-type-helpers
library as InstanceType
won’t work.
<script setup lang="ts">import { useTemplateRef } from 'vue'import MyGenericModal from './MyGenericModal.vue'import type { ComponentExposed } from 'vue-component-type-helpers'
const modal = useTemplateRef<ComponentExposed<typeof MyGenericModal>>('modal')
const openModal = () => { modal.value?.open('newValue')}</script>
With @vue/language-tools
2.1+, static template refs’ types can be automatically inferred and the above is only needed in edge cases.