TypeScript and Composition APIs
1. Mark the props type of the component
1.1 use<script setup>
A macro function supports inferring types from its arguments <script setup>
when using :defineProps()
<script setup lang="ts">
const props = defineProps({
foo: {
type: String, required: true },
bar: Number
})
props.foo // string
props.bar // number | undefined
</script>
This is called a "runtime declaration" because the arguments passed to will be used as options defineProps()
at runtime .props
However, it is usually more straightforward to define the type of props via generic parameters:
<script setup lang="ts">
const props = defineProps<{
foo: string
bar?: number
}>()
</script>
This is called "type-based declaration". Whenever possible, the compiler tries to deduce equivalent runtime options from the type parameters. In this scenario, the runtime options compiled in our second example are exactly the same as the first one.
Either type-based declarations or runtime declarations can be used, but not both.
We could also move the type of props into a separate interface:
<script setup lang="ts">
interface Props {
foo: string
bar?: number
}
const props = defineProps<Props>()
</script>
grammatical restrictions
In order to generate correct runtime code, defineProps()
the generic parameter passed to must be one of the following:
- A type literal:
defineProps<{
/*... */ }>()
- A reference to an interface or object type literal in the same file :
interface Props {
/* ... */}
defineProps<Props>()
Interface or object literal types can contain type references imported from other files, however, a defineProps
generic parameter passed to cannot itself be an imported type:
import {
Props } from './other-file'
// 不支持!
defineProps<Props>()
This is because Vue components are compiled separately and the compiler currently does not grab imported files to analyze source types. We plan to address this limitation in a future release.
1.2 Props deconstruct default value
When using type-based declarations, we lose the ability to define default values for props. This can be solved with the currently experimental responsive syntactic sugar :
<script setup lang="ts">
interface Props {
foo: string
bar?: number
}
// 对 defineProps() 的响应性解构
// 默认值会被编译为等价的运行时选项
const {
foo, bar = 100 } = defineProps<Props>()
</script>
This behavior currently requires an explicit opt-in .
1.3 <script setup>
In non-scenes
If not used <script setup>
, must be used in order to enable type inference for props defineComponent()
. setup()
The type of the props object passed to is props
deduced from the options.
import {
defineComponent } from 'vue'
export default defineComponent({
props: {
message: String
},
setup(props) {
props.message // <-- 类型:string
}
})
2. Label the emits type of the component
In <script setup>
, emit
the type annotation of a function can also be done through a runtime declaration or a type declaration:
<script setup lang="ts">
// 运行时
const emit = defineEmits(['change', 'update'])
// 基于类型
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
</script>
The type parameter should be a type literal with a call signature . The type of this type literal is emit
the type of the returned function. We can see that type-based declarations give us more fine-grained control over the types of events that are fired.
If not used <script setup>
, the type of the function exposed in the setup context defineComponent()
can also be deduced from the option :emits
emit
import {
defineComponent } from 'vue'
export default defineComponent({
emits: ['change'],
setup(props, {
emit }) {
emit('change') // <-- 类型检查 / 自动补全
}
})
Three, for ref()
the label type
A ref will deduce its type from its initial value:
import {
ref } from 'vue'
// 推导出的类型:Ref<number>
const year = ref(2020)
// => TS Error: Type 'string' is not assignable to type 'number'.
year.value = '2020'
Sometimes we may want to specify a more complex type for the value inside the ref, by using Ref
this type:
import {
ref } from 'vue'
import type {
Ref } from 'vue'
const year: Ref<string | number> = ref('2020')
year.value = 2020 // 成功!
Alternatively, ref()
pass a generic parameter when calling to override the default inference behavior:
// 得到的类型:Ref<string | number>
const year = ref<string | number>('2020')
year.value = 2020 // 成功!
If you specify a generic parameter without an initial value, the end result will be a undefined
union type containing :
// 推导得到的类型:Ref<number | undefined>
const n = ref<number>()
4. reactive()
Label the type
reactive()
Also implicitly deduces types from its arguments:
import {
reactive } from 'vue'
// 推导得到的类型:{ title: string }
const book = reactive({
title: 'Vue 3 指引' })
To explicitly annotate reactive
the type of a variable, we can use interfaces:
import {
reactive } from 'vue'
interface Book {
title: string
year?: number
}
const book: Book = reactive({
title: 'Vue 3 指引' })
TIP: The generic parameter of is deprecated reactive()
, because the return value of deep ref unpacking is different from the type of the generic parameter.
Five, for computed()
the label type
computed()
The type is automatically deduced from the return value of its evaluation function:
import {
ref, computed } from 'vue'
const count = ref(0)
// 推导得到的类型: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 explicitly specify the type via a generic parameter:
const double = computed<number>(() => {
// 若返回值不是 number 类型则会报错
})
6. Mark the type of the event handler function
When handling native DOM events, we should properly annotate the types for the parameters we pass to event handlers. Let's look at this example:
<script setup lang="ts">
function handleChange(event) {
// `event` 隐式地标注为 `any` 类型
console.log(event.target.value)
}
</script>
<template>
<input type="text" @change="handleChange" />
</template>
When not type-annotated, the event
parameter is implicitly annotated with any
type. This will also throw a TS error when tsconfig.json
configured in "strict": true
or . "noImplicitAny": true
Therefore, it is recommended to explicitly type the parameters of event handler functions. Also, you may need to explicitly cast event
properties on the :
function handleChange(event: Event) {
console.log((event.target as HTMLInputElement).value)
}
Seven, mark the type for provide / inject
provide and inject will usually run in different components. To correctly mark the type of the injected value, Vue provides an InjectionKey
interface, which is a Symbol
generic type inherited from , which can be used to synchronize 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') // 若提供的是非字符串值会导致错误
const foo = inject(key) // foo 的类型:string | undefined
It is recommended to put the injected key type in a separate file so that it can be imported by multiple components.
When injecting a key with a string, the type of the injected value unknown
needs to be explicitly declared through a generic parameter:
const foo = inject<string>('foo') // 类型:string | undefined
Note that the injected value can still be undefined
, because there is no guarantee that the provider will provide this value at runtime.
When a default value is provided, the undefined
type can be removed:
const foo = inject<string>('foo', 'bar') // 类型:string
You can also cast the value if you are sure the value will always be provided:
const foo = inject('foo') as string
8. Annotate the template reference type
Template references need to be created with an explicitly specified generic parameter and an initial value 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>
Note that for strict type safety it is necessary el.value
to use optional chaining or type guards when accessing . This is because the value of this ref is initial until the component is mounted null
, and v-if
can also be set to when the referenced element is unmounted due to the behavior of null
.
Nine, refer to the annotation type for the component template
Sometimes, you may need to add a template reference to a child component in order to call methods it exposes. For example, let's say we have a MyModal
child component that has a method that opens a modal:
<!-- MyModal.vue -->
<script setup lang="ts">
import {
ref } from 'vue'
const isContentShown = ref(false)
const open = () => (isContentShown.value = true)
defineExpose({
open
})
</script>
In order to obtain MyModal
the type of , we first need to typeof
obtain its type through , and then use TypeScript's built-in InstanceType
tool type to obtain its instance type:
<!-- App.vue -->
<script setup lang="ts">
import MyModal from './MyModal.vue'
const modal = ref<InstanceType<typeof MyModal> | null>(null)
const openModal = () => {
modal.value?.open()
}
</script>