TypeScript 与选项式 API {#typescript-with-options-api}

这一章假设你已经阅读了搭配 TypeScript 使用 Vue 的概览。

:::tip 虽然 Vue 的确支持在选项式 API 中使用 TypeScript,但还是推荐通过 TypeScript 与组合式 API 来使用 Vue,因为它提供了更简单、更高效和更可靠的类型推导。 :::

为组件的 prop 标注类型 {#typing-component-props}

选项式 API 中对 prop 的类型推导需要用 defineComponent() 来包装组件。有了它,Vue 才可以通过 props 以及一些额外的选项,比如 required: truedefault 来推导出 prop 的类型:

  1. import { defineComponent } from 'vue'
  2. export default defineComponent({
  3. // 启用了类型推导
  4. props: {
  5. name: String,
  6. id: [Number, String],
  7. msg: { type: String, required: true },
  8. metadata: null
  9. },
  10. mounted() {
  11. this.name // 类型:string | undefined
  12. this.id // 类型:number | string | undefined
  13. this.msg // 类型:string
  14. this.metadata // 类型:any
  15. }
  16. })

然而,这种运行时 props 选项仅支持使用构造函数来作为一个 prop 的类型——没有办法指定多层级对象或函数签名之类的复杂类型。

我们可以使用 PropType 这个工具类型来标记更复杂的 prop 类型:

  1. import { defineComponent, PropType } from 'vue'
  2. interface Book {
  3. title: string
  4. author: string
  5. year: number
  6. }
  7. export default defineComponent({
  8. props: {
  9. book: {
  10. // 提供相对 `Object` 更确定的类型
  11. type: Object as PropType<Book>,
  12. required: true
  13. },
  14. // 也可以标记函数
  15. callback: Function as PropType<(id: number) => void>
  16. },
  17. mounted() {
  18. this.book.title // string
  19. this.book.year // number
  20. // TS Error: argument of type 'string' is not
  21. // assignable to parameter of type 'number'
  22. this.callback?.('123')
  23. }
  24. })

注意事项 {#caveats}

因为一个 TypeScript 的 设计限制,你在使用函数作为 prop 的 validatordefault 选项值时需要格外小心——确保使用箭头函数:

  1. import { defineComponent, PropType } from 'vue'
  2. interface Book {
  3. title: string
  4. year?: number
  5. }
  6. export default defineComponent({
  7. props: {
  8. bookA: {
  9. type: Object as PropType<Book>,
  10. // 确保使用箭头函数
  11. default: () => ({
  12. title: 'Arrow Function Expression'
  13. }),
  14. validator: (book: Book) => !!book.title
  15. }
  16. }
  17. })

这会防止 Typescript 将 this 根据函数内的环境作出不符合我们期望的类型推导。

为组件的 emit 标注类型 {#typing-component-emits}

我们可以为使用了对象语法作为 emits 选项所触发的事件声明期望的载荷内容类型。并且,所有未声明的事件调用时都会抛出一个类型错误:

  1. import { defineComponent } from 'vue'
  2. export default defineComponent({
  3. emits: {
  4. addBook(payload: { bookName: string }) {
  5. // 执行运行时校验
  6. return payload.bookName.length > 0
  7. }
  8. },
  9. methods: {
  10. onSubmit() {
  11. this.$emit('addBook', {
  12. bookName: 123 // 类型错误
  13. })
  14. this.$emit('non-declared-event') // 类型错误
  15. }
  16. }
  17. })

为计算属性标记类型 {#typing-computed-properties}

一个计算属性根据其返回值来推导其类型:

  1. import { defineComponent } from 'vue'
  2. export default defineComponent({
  3. data() {
  4. return {
  5. message: 'Hello!'
  6. }
  7. },
  8. computed: {
  9. greeting() {
  10. return this.message + '!'
  11. }
  12. },
  13. mounted() {
  14. this.greeting // 类型:string
  15. }
  16. })

在某些场景中,你可能想要显式地标记出计算属性的类型以确保其实现是正确的:

  1. import { defineComponent } from 'vue'
  2. export default defineComponent({
  3. data() {
  4. return {
  5. message: 'Hello!'
  6. }
  7. },
  8. computed: {
  9. // 显式标注返回类型
  10. greeting(): string {
  11. return this.message + '!'
  12. },
  13. // 标注一个可写的计算属性
  14. greetingUppercased: {
  15. get(): string {
  16. return this.greeting.toUpperCase()
  17. },
  18. set(newValue: string) {
  19. this.message = newValue.toUpperCase()
  20. }
  21. }
  22. }
  23. })

在某些 TypeScript 因循环引用而无法推导类型的情况下,可能必须进行显式的类型标注。

为事件处理器标注类型 {#typing-event-handlers}

在处理原生 DOM 事件时,应该为我们传递给事件处理器的参数正确地标注类型。让我们看一下这个例子:

  1. <script lang="ts">
  2. import { defineComponent } from 'vue'
  3. export default defineComponent({
  4. methods: {
  5. handleChange(event) {
  6. // `event` 隐式地标注为 `any` 类型
  7. console.log(event.target.value)
  8. }
  9. }
  10. })
  11. </script>
  12. <template>
  13. <input type="text" @change="handleChange" />
  14. </template>

没有类型标注时,这个 event 参数会隐式地标注为 any 类型。这也会在 tsconfig.json 中配置了 "strict": true"noImplicitAny": true 时抛出一个 TS 错误。因此,建议显式地为事件处理器的参数标注类型。此外,你可能需要显式地强制转换 event 上的 property:

  1. import { defineComponent } from 'vue'
  2. export default defineComponent({
  3. methods: {
  4. handleChange(event: Event) {
  5. console.log((event.target as HTMLInputElement).value)
  6. }
  7. }
  8. })

扩充全局 property {#augmenting-global-properties}

某些插件通过 app.config.globalProperties 为所有组件都安装了全局可用的 property。举个例子,我们可能为了请求数据而安装了 this.$http,或者为了国际化而安装了 this.$translate。为了使 TypeScript 更好地支持这个行为,Vue 暴露了一个被设计为可以通过 TypeScript 模块扩充来扩充的 ComponentCustomProperties 接口:

  1. import axios from 'axios'
  2. declare module 'vue' {
  3. interface ComponentCustomProperties {
  4. $http: typeof axios
  5. $translate: (key: string) => string
  6. }
  7. }

参考:

类型扩充的位置 {#type-augmentation-placement}

我们可以将这些类型扩充放在一个 .ts 文件,或是一个以整个项目为范围的 *.d.ts 文件中。无论哪一种,确保在 tsconfig.json 中将其引入。对于库或插件作者,这个文件应该在 package.jsontype property 中被列出。

为了利用模块扩充的优势,你需要确保将扩充的模块放在 TypeScript 模块 中。 也就是说,该文件需要包含至少一个顶级的 importexport,即使它只是 export {}。如果扩充被放在模块之外,它将覆盖原始类型,而不是扩充!

扩充自定义选项 {#augmenting-custom-options}

某些插件,比如 vue-router,提供了一些自定义的组件选项,比如 beforeRouteEnter

  1. import { defineComponent } from 'vue'
  2. export default defineComponent({
  3. beforeRouteEnter(to, from, next) {
  4. // ...
  5. }
  6. })

如果没有确切的类型标注,这个钩子函数的参数会隐式地标注为 any 类型。我们可以为 ComponentCustomOptions 接口扩充自定义的选项来支持:

  1. import { Route } from 'vue-router'
  2. declare module 'vue' {
  3. interface ComponentCustomOptions {
  4. beforeRouteEnter?(to: Route, from: Route, next: () => void): void
  5. }
  6. }

现在这个 beforeRouterEnter 选项会被准确地标注类型。注意这只是一个例子——像 vue-router 这种类型完备的库应该在它们自己的类型定义中自动执行这些扩充。

这种类型扩充和全局 property 扩充受到相同的限制

参考: