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 这种类型完备的库应该在它们自己的类型定义中自动执行这些扩展。

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


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

相关内容

热门资讯

安卓版pc端系统,跨越平台界限... 你有没有想过,你的安卓手机里的应用,竟然可以在电脑上无缝运行?没错,这就是安卓版PC端系统的魅力所在...
安卓7车机系统,科技与安全的完... 你有没有发现,现在的汽车越来越智能了?没错,我说的就是那些内置了安卓7车机系统的家伙们。想象当你坐在...
王者荣耀安卓系统区别,深度揭秘... 你有没有发现,玩王者荣耀的时候,安卓系统的手机和苹果系统的手机,感觉就像是两个不同的世界呢?今天,就...
盒子电视安卓9系统,畅享智能新... 亲爱的读者们,你是否曾为拥有一台功能强大、系统流畅的电视而心动?今天,我要给你介绍一款特别受欢迎的盒...
智慧吴江app安卓系统,安卓系... 你知道吗?最近吴江地区掀起了一股智慧风潮,一款名为“智慧吴江app”的应用在安卓系统上大受欢迎。这款...
苹果系统听歌app安卓,跨平台... 你有没有发现,无论是走在街头还是坐在家里,音乐总是能瞬间点燃我们的心情?而在这个音乐无处不在的时代,...
安卓系统卡顿根源,性能瓶颈与优... 手机用久了是不是感觉越来越卡?是不是每次打开应用都要等半天,甚至有时候直接卡死?别急,今天就来跟你聊...
电脑系统怎么装安卓系统,电脑系... 你有没有想过,把安卓系统装在你的电脑上,是不是就像给电脑穿上了时尚的新衣呢?想象你可以在电脑上直接使...
安卓系统华为手环app,健康管... 你有没有发现,现在的生活越来越离不开智能设备了?手机、平板、手表……这些小玩意儿不仅让我们的生活变得...
switch lite刷安卓系... 你有没有想过,你的Switch Lite除了玩那些可爱的任天堂游戏,还能干些什么呢?没错,今天我要给...
想买华为但是安卓系统,尽享安卓... 最近是不是也被华为的新款手机给迷住了?看着那流畅的线条和强大的性能,是不是心动了呢?但是,一想到安卓...
怎么拷安卓系统文件,安卓系统文... 你有没有想过,手机里的那些安卓系统文件,其实就像是一扇通往手机世界的秘密通道呢?想要深入了解你的安卓...
安卓系统移植按键失灵,安卓系统... 最近你的安卓手机是不是也遇到了按键失灵的尴尬情况呢?这可真是让人头疼啊!别急,今天就来给你详细解析一...
安卓系统更新管理在哪,全面解析... 你有没有发现,你的安卓手机最近是不是总在提醒你更新系统呢?别急,别急,今天就来手把手教你,安卓系统更...
安卓系统哪里出的,从诞生地到全... 你有没有想过,我们每天离不开的安卓系统,它究竟是从哪里冒出来的呢?是不是觉得这个问题有点儿像是在问星...
最好的电脑安卓系统,最佳电脑安... 亲爱的电脑迷们,你是否在寻找一款既能满足你工作需求,又能让你畅享娱乐的电脑操作系统呢?今天,我要给你...
安卓系统保密性,守护隐私的坚实... 你知道吗?在这个信息爆炸的时代,保护个人隐私变得比以往任何时候都重要。尤其是对于安卓系统用户来说,了...
苹果系统下载安卓版本,安卓版本... 你有没有想过,为什么苹果系统的手机那么受欢迎,却还有人想要下载安卓版本呢?这背后可是有着不少故事呢!...
安卓系统如何下载carplay... 你是不是也和我一样,对安卓系统上的CarPlay功能充满了好奇?想象在安卓手机上就能享受到苹果Car...
退回安卓系统的理由,揭秘安卓系... 你有没有想过,为什么有些人会选择退回到安卓系统呢?这可不是一件简单的事情,背后可是有着不少原因哦!让...