Vue 之 vue3 与 TS 的配合使用整理
admin
2024-02-07 23:28:24
0

目录

  • 说明
  • 前言
    • 配置 tsconfig.json
  • 正文
    • TypeScript 与组合式 API
      • 为组件的 props 标注类型
        • 使用 `

          这被称之为“运行时声明”,因为传递给 defineProps() 的参数会作为运行时的 props 选项使用。

          然而,通过泛型参数来定义 props 的类型通常更直接:

          
          

          这被称之为“基于类型的声明”。编译器会尽可能地尝试根据类型参数推导出等价的运行时选项。在这种场景下,我们第二个例子中编译出的运行时选项和第一个是完全一致的。

          基于类型的声明或者运行时声明可以择一使用,但是不能同时使用。

          我们也可以将 props 的类型移入一个单独的接口中:

          
          

          语法限制

          为了生成正确的运行时代码,传给 defineProps() 的泛型参数必须是以下之一:

          • 一个类型字面量:
          defineProps<{ /*... */ }>()
          
          • 同一个文件中的一个接口或对象类型字面量的引用:
          interface Props {/* ... */}defineProps()
          

          接口或对象字面类型可以包含从其他文件导入的类型引用,但是,传递给 defineProps 的泛型参数本身不能是一个导入的类型:

          import { Props } from './other-file'// 不支持!
          defineProps()
          

          这是因为 Vue 组件是单独编译的,编译器目前不会抓取导入的文件以分析源类型。我们计划在未来的版本中解决这个限制。

          Props 解构默认值

          当使用基于类型的声明时,我们失去了为 props 声明默认值的能力。这可以通过 withDefaults 编译器宏解决:

          export interface Props {msg?: stringlabels?: string[]
          }const props = withDefaults(defineProps(), {msg: 'hello',labels: () => ['one', 'two']
          })
          

          这将被编译为等效的运行时 props default 选项。此外,withDefaults 帮助程序为默认值提供类型检查,并确保返回的 props 类型删除了已声明默认值的属性的可选标志。

          或者,你可以使用目前为实验性的响应性语法糖:

          
          

          这个行为目前需要显式地选择开启。

          这个类型参数应该是一个带调用签名的类型字面量。这个类型字面量的类型就是返回的 emit 函数的类型。我们可以看到,基于类型的声明使我们可以对所触发事件的类型进行更细粒度的控制。

          若没有使用

          
          

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

          function handleChange(event: Event) {console.log((event.target as HTMLInputElement).value)
          }
          

          为 provide / inject 标注类型

          provide 和 inject 通常会在不同的组件中运行。要正确地为注入的值标记类型,Vue 提供了一个 InjectionKey 接口,它是一个继承自 Symbol 的泛型类型,可以用来在提供者和消费者之间同步注入值的类型:

          import { provide, inject } from 'vue'
          import type { InjectionKey } from 'vue'const key = Symbol() as InjectionKeyprovide(key, 'foo') // 若提供的是非字符串值会导致错误const foo = inject(key) // foo 的类型:string | undefined
          

          建议将注入 key 的类型放在一个单独的文件中,这样它就可以被多个组件导入。

          当使用字符串注入 key 时,注入值的类型是 unknown,需要通过泛型参数显式声明:

          const foo = inject('foo') // 类型:string | undefined
          

          注意注入的值仍然可以是 undefined,因为无法保证提供者一定会在运行时 provide 这个值。

          当提供了一个默认值后,这个 undefined 类型就可以被移除:

          const foo = inject('foo', 'bar') // 类型:string
          

          如果你确定该值将始终被提供,则还可以强制转换该值:

          const foo = inject('foo') as string
          

          为模板引用标注类型

          模板引用需要通过一个显式指定的泛型参数和一个初始值 null 来创建:

          
          
          
          

          注意为了严格的类型安全,有必要在访问 el.value 时使用可选链或类型守卫。这是因为直到组件被挂载前,这个 ref 的值都是初始的 null,并且在由于 v-if 的行为将引用的元素卸载时也可以被设置为 null。

          为组件模板引用标注类型

          有时,你可能需要为一个子组件添加一个模板引用,以便调用它公开的方法。举例来说,我们有一个 MyModal 子组件,它有一个打开模态框的方法:

          
          
          

          为了获取 MyModal 的类型,我们首先需要通过 typeof 得到其类型,再使用 TypeScript 内置的 InstanceType 工具类型来获取其实例类型:

          
          
          

          注意,如果你想在 TypeScript 文件而不是在 Vue SFC 中使用这种技巧,需要开启 Volar 的 Takeover 模式。


          TypeScript 与选项式 API

          为组件的 props 标注类型

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

          import { defineComponent } from 'vue'export default defineComponent({// 启用了类型推导props: {name: String,id: [Number, String],msg: { type: String, required: true },metadata: null},mounted() {this.name // 类型:string | undefinedthis.id // 类型:number | string | undefinedthis.msg // 类型:stringthis.metadata // 类型:any}
          })
          

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

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

          import { defineComponent } from 'vue'
          import type { PropType } from 'vue'interface Book {title: stringauthor: stringyear: number
          }export default defineComponent({props: {book: {// 提供相对 `Object` 更确定的类型type: Object as PropType,required: true},// 也可以标记函数callback: Function as PropType<(id: number) => void>},mounted() {this.book.title // stringthis.book.year // number// TS Error: argument of type 'string' is not// assignable to parameter of type 'number'this.callback?.('123')}
          })
          

          注意事项 如果你的 TypeScript 版本低于 4.7,在使用函数作为 prop 的 validator 和 default
          选项值时需要格外小心——确保使用箭头函数:

          import { defineComponent } from 'vue'
          import type { PropType } from 'vue'interface Book {title: stringyear?: number
          }export default defineComponent({props: {bookA: {type: Object as PropType,// 如果你的 TypeScript 版本低于 4.7,确保使用箭头函数default: () => ({title: 'Arrow Function Expression'}),validator: (book: Book) => !!book.title}}
          })
          

          这会防止 TypeScript 将 this 根据函数内的环境作出不符合我们期望的类型推导。这是之前版本的一个设计限制,不过现在已经在
          TypeScript 4.7 中解决了。

          为组件的 emits 标注类型

          我们可以给 emits 选项提供一个对象来声明组件所触发的事件,以及这些事件所期望的参数类型。试图触发未声明的事件会抛出一个类型错误:

          import { defineComponent } from 'vue'export default defineComponent({emits: {addBook(payload: { bookName: string }) {// 执行运行时校验return payload.bookName.length > 0}},methods: {onSubmit() {this.$emit('addBook', {bookName: 123 // 类型错误})this.$emit('non-declared-event') // 类型错误}}
          })
          

          为计算属性标记类型

          计算属性会自动根据其返回值来推导其类型:

          import { defineComponent } from 'vue'export default defineComponent({data() {return {message: 'Hello!'}},computed: {greeting() {return this.message + '!'}},mounted() {this.greeting // 类型:string}
          })
          

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

          import { defineComponent } from 'vue'export default defineComponent({data() {return {message: 'Hello!'}},computed: {// 显式标注返回类型greeting(): string {return this.message + '!'},// 标注一个可写的计算属性greetingUppercased: {get(): string {return this.greeting.toUpperCase()},set(newValue: string) {this.message = newValue.toUpperCase()}}}
          })
          

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

          为事件处理函数标注类型

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

          
          
          
          

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

          import { defineComponent } from 'vue'export default defineComponent({methods: {handleChange(event: Event) {console.log((event.target as HTMLInputElement).value)}}
          })
          

          扩展全局属性

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

          import axios from 'axios'declare module 'vue' {interface ComponentCustomProperties {$http: typeof axios$translate: (key: string) => string}
          }
          

          类型扩展的位置

          我们可以将这些类型扩展放在一个 .ts 文件,或是一个影响整个项目的 *.d.ts 文件中。无论哪一种,都应确保在 tsconfig.json 中包括了此文件。对于库或插件作者,这个文件应该在 package.json 的 types 属性中被列出。

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

          // 不工作,将覆盖原始类型。
          declare module 'vue' {interface ComponentCustomProperties {$translate: (key: string) => string}
          }
          
          // 正常工作。
          export {}declare module 'vue' {interface ComponentCustomProperties {$translate: (key: string) => string}
          }
          

          扩展自定义选项

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

          import { defineComponent } from 'vue'export default defineComponent({beforeRouteEnter(to, from, next) {// ...}
          })
          

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

          import { Route } from 'vue-router'declare module 'vue' {interface ComponentCustomOptions {beforeRouteEnter?(to: Route, from: Route, next: () => void): void}
          }
          

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

          这种类型扩展和全局属性扩展受到相同的限制。


          如有不足,望大家多多指点! 谢谢!

相关内容

热门资讯

安卓系统呼吸灯作用,探索安卓系... 你知道吗?你的安卓手机里有个神奇的小功能,它就像你的私人小秘书,时刻关注着你的生活节奏。没错,说的就...
安卓系统的设置停止,自动生成文... 亲爱的手机控们,你是否也有过这样的经历:手机里装了各种应用,可就是觉得运行速度越来越慢,有时候甚至卡...
魅动app安卓系统,安卓系统下... 你有没有发现,最近手机上多了一个叫“魅动app”的小家伙?它可是安卓系统里的一股清流,让人爱不释手。...
安卓怎么玩电脑系统,跨平台操作... 你是不是也和我一样,对安卓系统爱得深沉,但又想体验一下电脑系统的强大呢?别急,今天就来手把手教你如何...
安卓系统买什么品牌好用,盘点几... 你最近是不是在为选择一款好用的安卓手机而烦恼呢?市面上品牌繁多,让人眼花缭乱。别急,今天我就来给你详...
安卓系统怎样不卡顿,尽享丝滑体... 手机用久了是不是感觉越来越卡?别急,今天就来跟你聊聊安卓系统怎样不卡顿,让你的手机焕发新生!一、清理...
vidaa3是安卓系统,安卓系... 你有没有听说过Vidaa3这款智能电视?它可是最近在市场上掀起了一股热潮呢!你知道吗,Vidaa3搭...
安卓系统怎么看显存,安卓系统显... 你有没有发现,手机用久了,有时候打开个游戏或者应用,突然卡顿了是不是觉得有点不爽?别急,今天就来教你...
上海刷安卓原生系统下载,安卓原... 你有没有想过,你的安卓手机系统是不是该更新一下了呢?尤其是当你身处繁华的上海,想要体验最新的科技魅力...
麒麟系统不也是安卓吗,揭秘其与... 你有没有想过,那个听起来高大上的麒麟系统,其实跟安卓也没那么大的区别呢?没错,今天就来聊聊这个话题,...
安卓系统升级关不掉,安卓系统升... 你是不是也遇到过这种情况?手机屏幕上突然弹出一个安卓系统升级的提示,你满怀期待地点击了“立即升级”,...
手机系统不就苹果安卓吗,苹果与... 手机系统不就苹果安卓吗?亲爱的读者们,你是否曾在手机的选择上犹豫不决,尤其是面对那两个如雷贯耳的名字...
智能柜用安卓系统网络,智能柜安... 你有没有想过,那些在商场、地铁站、学校门口随处可见的智能柜,竟然是运用了安卓系统来掌控它们的网络呢?...
云原神需要安卓系统,安卓系统下... 你有没有发现,最近云原神这款游戏在玩家圈子里可是火得一塌糊涂呢!不少小伙伴都在问,这款游戏到底需要什...
安卓系统需要自己注册吗,轻松实... 你有没有想过,当你第一次拿到那台闪闪发光的安卓手机时,是不是觉得整个世界都变得触手可及了呢?没错,安...
5s运行安卓系统,高效体验与卓... 你知道吗?最近在科技圈里,有一款运行安卓系统的设备引起了大家的热议。没错,就是5S运行安卓系统!这可...
安卓系统苹果手表app,苹果手... 你知道吗?现在科技的发展真是让人眼花缭乱,尤其是智能手机和可穿戴设备。今天,咱们就来聊聊安卓系统和苹...
安卓优化系统哪个厂家好,哪家厂... 你有没有发现,手机用久了,速度越来越慢,就像老牛拉车一样吃力?别急,今天就来聊聊安卓优化系统哪个厂家...
安卓系统低配单机游戏,畅享乐趣 你有没有发现,即使是在安卓系统的低配手机上,也能玩到不少好玩的单机游戏呢?没错,今天就来给你好好盘点...
安卓系统有多烂,为何有人称其“... 你有没有发现,安卓系统最近好像有点儿“烂”呢?别急,不是那种烂到无法使用的烂,而是说它在某些方面确实...