跳转到内容

使用组合式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>

类型参数可以是以下之一

  1. 一个可调用函数类型,但写成具有调用签名的类型字面量。它将用作返回的emit函数的类型。
  2. 一个类型字面量,其中键是事件名称,值是表示事件额外接受参数的数组/元组类型。上面的示例使用了命名元组,因此每个参数都可以有一个显式的名称。

正如我们所见,类型声明使我们能够对发射事件的类型约束有更细粒度的控制。

当不使用 <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+ 开始,静态模板引用的类型可以自动推断,上述内容仅在边缘情况下需要。

Composition API 的 TypeScript 已加载