使用组合式API的TypeScript
此页面假设您已经阅读了使用TypeScript与Vue的概述。
组件属性类型化
使用 <script setup>
当使用 <script setup>
时,defineProps()
宏支持根据其参数推断props类型
vue
<script setup lang="ts">
const props = defineProps({
foo: { type: String, required: true },
bar: Number
})
props.foo // string
props.bar // number | undefined
</script>
这被称为“运行时声明”,因为传递给 defineProps()
的参数将被用作运行时 props
选项。
但是,通常通过泛型类型参数定义props会更直接
vue
<script setup lang="ts">
const props = defineProps<{
foo: string
bar?: number
}>()
</script>
这被称为“类型声明”。编译器将尽力根据类型参数推断等价的运行时选项。在这种情况下,我们的第二个示例编译成了与第一个示例完全相同的运行时选项。
您可以使用类型声明或运行时声明中的任何一个,但不能同时使用两个。
我们还可以将props类型移动到单独的接口中
vue
<script setup lang="ts">
interface Props {
foo: string
bar?: number
}
const props = defineProps<Props>()
</script>
如果 Props
从外部源导入,这也同样适用。此功能要求TypeScript是Vue的依赖项。
vue
<script setup lang="ts">
import type { Props } from './foo'
const props = defineProps<Props>()
</script>
语法限制
在版本3.2及以下版本中,defineProps()
的泛型类型参数限制为类型字面或局部接口的引用。
这个限制在3.3版本中已解决。Vue的最新版本支持在类型参数位置引用导入的有限数量的复杂类型。然而,由于类型到运行时的转换仍然是基于AST的,因此不支持需要实际类型分析的某些复杂类型,例如条件类型。您可以为单个prop使用条件类型,但不能为整个props对象使用。
属性默认值
当使用基于类型的声明时,我们失去了声明属性默认值的能力。可以通过使用响应式属性解构 来解决这个问题。
ts
interface Props {
msg?: string
labels?: string[]
}
const { msg = 'hello', labels = ['one', 'two'] } = defineProps<Props>()
在3.4及以下版本中,默认未启用响应式属性解构。一种替代方法是使用withDefaults
编译器宏
ts
interface Props {
msg?: string
labels?: string[]
}
const props = withDefaults(defineProps<Props>(), {
msg: 'hello',
labels: () => ['one', 'two']
})
这将编译为等效的运行时属性default
选项。此外,withDefaults
辅助函数提供了对默认值的类型检查,并确保返回的props
类型已移除具有默认值的属性的必需标志。
INFO
请注意,当使用withDefaults
时,可变引用类型的默认值(如数组或对象)应使用函数包装,以避免意外修改和外部副作用。这确保每个组件实例都获得自己的默认值副本。在使用解构时使用默认值时,这不是必要的。
没有<script setup>
如果不使用<script setup>
,则必须使用defineComponent()
来启用属性类型推断。传递给setup()
的属性对象类型从props
选项推断。
ts
import { defineComponent } from 'vue'
export default defineComponent({
props: {
message: String
},
setup(props) {
props.message // <-- type: string
}
})
复杂属性类型
使用基于类型的声明时,属性可以使用与任何其他类型类似的复杂类型
vue
<script setup lang="ts">
interface Book {
title: string
author: string
year: number
}
const props = defineProps<{
book: Book
}>()
</script>
对于运行时声明,我们可以使用PropType
实用类型
ts
import type { PropType } from 'vue'
const props = defineProps({
book: Object as PropType<Book>
})
如果我们直接指定props
选项,它将以相同的方式工作
ts
import { defineComponent } from 'vue'
import type { PropType } from 'vue'
export default defineComponent({
props: {
book: Object as PropType<Book>
}
})
props
选项更常与Options API一起使用,因此您将在TypeScript与Options API的指南中找到更多详细示例。那些示例中显示的技术也适用于使用defineProps()
进行的运行时声明。
为组件发射进行类型定义
在<script setup>
中,也可以使用运行时声明或类型声明来对emit
函数进行类型定义
vue
<script setup lang="ts">
// runtime
const emit = defineEmits(['change', 'update'])
// options based
const 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-based
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
// 3.3+: alternative, more succinct syntax
const emit = defineEmits<{
change: [id: number]
update: [value: string]
}>()
</script>
类型参数可以是以下之一
- 一个可调用函数类型,但写成具有调用签名的类型字面量。它将用作返回的
emit
函数的类型。 - 一个类型字面量,其中键是事件名称,值是表示事件额外接受参数的数组/元组类型。上面的示例使用了命名元组,因此每个参数都可以有一个显式的名称。
正如我们所见,类型声明使我们能够对发射事件的类型约束有更细粒度的控制。
当不使用 <script setup>
时,defineComponent()
可以推断 setup 上下文中暴露的 emit
函数允许的事件。
ts
import { defineComponent } from 'vue'
export default defineComponent({
emits: ['change'],
setup(props, { emit }) {
emit('change') // <-- type check / auto-completion
}
})
输入 ref()
引用会从初始值推断类型。
ts
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'
有时我们可能需要为引用的内部值指定复杂类型。我们可以通过使用 Ref
类型来实现这一点。
ts
import { ref } from 'vue'
import type { Ref } from 'vue'
const year: Ref<string | number> = ref('2020')
year.value = 2020 // ok!
或者,在调用 ref()
时传递泛型参数以覆盖默认推断。
ts
// resulting type: Ref<string | number>
const year = ref<string | number>('2020')
year.value = 2020 // ok!
如果您指定了泛型类型参数但省略了初始值,则结果类型将是一个包含 undefined
的联合类型。
ts
// inferred type: Ref<number | undefined>
const n = ref<number>()
输入 reactive()
reactive()
也会从其参数隐式推断类型。
ts
import { reactive } from 'vue'
// inferred type: { title: string }
const book = reactive({ title: 'Vue 3 Guide' })
要显式类型化 reactive
属性,我们可以使用接口。
ts
import { reactive } from 'vue'
interface Book {
title: string
year?: number
}
const book: Book = reactive({ title: 'Vue 3 Guide' })
提示
不建议使用 reactive()
的泛型参数,因为返回的类型,它处理嵌套引用展开,与泛型参数类型不同。
输入 computed()
computed()
根据获取器的返回值推断其类型。
ts
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('')
您也可以通过泛型参数指定显式类型。
ts
const double = computed<number>(() => {
// type error if this doesn't return a number
})
输入事件处理程序
处理原生 DOM 事件时,正确地类型化传递给处理程序的参数可能很有用。让我们看看这个例子。
vue
<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>
没有类型注释,event
参数将隐式具有 any
类型。如果使用 tsconfig.json
中的 "strict": true
或 "noImplicitAny": true
,这将导致 TS 错误。因此,建议显式注释事件处理程序的参数。此外,您可能需要在使用 event
的属性时使用类型断言。
ts
function handleChange(event: Event) {
console.log((event.target as HTMLInputElement).value)
}
输入 Provide / Inject
提供和注入通常在单独的组件中执行。为了正确类型化注入的值,Vue 提供了一个 InjectionKey
接口,它是一个扩展 Symbol
的泛型类型。它可以用来在提供者和消费者之间同步注入值的类型。
ts
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
建议将注入密钥放在单独的文件中,以便可以在多个组件中导入。
当使用字符串注入密钥时,注入值的类型将是 unknown
,需要通过泛型类型参数显式声明。
ts
const foo = inject<string>('foo') // type: string | undefined
注意注入的值仍然可以是 undefined
,因为没有保证提供者会在运行时提供此值。
可以通过提供默认值来删除 undefined
类型。
ts
const foo = inject<string>('foo', 'bar') // type: string
如果您确定值始终被提供,也可以强制转换值。
ts
const foo = inject('foo') as string
输入模板引用
使用 Vue 3.5 和 @vue/language-tools
2.1(为 IDE 语言服务和 vue-tsc
提供动力),在 SFC 中由 useTemplateRef()
创建的引用的类型可以根据匹配的 ref
属性使用的元素自动推断静态引用。
在无法自动推断的情况下,您仍然可以通过泛型参数将模板引用强制转换为显式类型。
ts
const el = useTemplateRef<HTMLInputElement>(null)
3.5 版本之前的使用方法
模板引用应该使用显式泛型类型参数和初始值 null
创建。
vue
<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>
要获取正确的 DOM 接口,您可以检查类似 MDN 的页面。
请注意,为了确保严格的类型安全,在访问 el.value
时需要使用可选链或类型守卫。这是因为直到组件挂载,初始引用值是 null
,并且如果引用的元素通过 v-if
被卸载,它也可以被设置为 null
。
组件模板引用的类型化
从 Vue 3.5 和 @vue/language-tools
2.1(为 IDE 语言服务和 vue-tsc
提供支持)开始,SFC 中使用 useTemplateRef()
创建的引用的类型可以根据匹配的 ref
属性所使用的元素或组件自动推断为静态引用。
在无法自动推断的情况下(例如,非 SFC 使用或动态组件),您仍然可以通过泛型参数将模板引用转换为显式类型。
为了获取导入组件的实例类型,我们首先需要通过 typeof
获取其类型,然后使用 TypeScript 的内置 InstanceType
工具提取其实例类型。
vue
<!-- App.vue -->
<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>
在组件的确切类型不可用或不重要的情况下,可以使用 ComponentPublicInstance
。这将只包含所有组件都共享的属性,例如 $el
。
ts
import { useTemplateRef } from 'vue'
import type { ComponentPublicInstance } from 'vue'
const child = useTemplateRef<ComponentPublicInstance | null>(null)
当引用的组件是一个泛型组件时,例如 MyGenericModal
。
vue
<!-- MyGenericModal.vue -->
<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>
需要使用来自 vue-component-type-helpers
库的 ComponentExposed
来引用,因为 InstanceType
无法使用。
vue
<!-- App.vue -->
<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>>(null)
const openModal = () => {
modal.value?.open('newValue')
}
</script>
请注意,从 @vue/language-tools
2.1+ 开始,静态模板引用的类型可以自动推断,上述内容仅在边缘情况下需要。