一文快速熟悉vue3中如何正确做ts类型标记

前言

vue3已经出来三年多了,其中新增了组合式 api,并且也提供了更好的 TypeScript类型标记支持,之后还推出了

这种声明方式基本和 选项式api的声明方式一致。

1.2 运行时-复杂类型声明

对于运行时声明,有时候我们需要给一个 props参数声明为一个 自定义接口类型,这时需要借助 vue内部提供的 PropType工具类型进行类型断言:

ts代码解读复制代码 import type { IUser } from './types.ts' import type { PropType } from 'vue'  const props = defineProps({   data: {     type: Object as PropType,     required: true   } })  props.data  // data: IUser 

2.基于类型的声明

基于类型的声明在编译保存代码编译阶段就能确定类型,获得更好的类型提示。通过泛型参数来定义 props的类型比较常用,通过这种方式定义的类型看起来也更加直观。

ts代码解读复制代码 import type { IUser } from './types.ts' const props = defineProps<{   title?: string,   size: 'mini' | 'default' | 'large',   data: IUser }>()  props.title // title?: string | undefine props.size  // size: "default" | "mini" | "large" props.data  // data: IUser 

以上我们通过泛型定义的参数和上面基于运行时声明参数是一样的,但是明显可以看出这样写简洁了许多。

3.withDefaults定义参数默认值

基于类型的声明在给参数赋默认值时,需要借助 withDefaults 编译器宏来实现。

ts代码解读复制代码 import type { IUser } from './types.ts' const props = withDefaults(defineProps<{   title?: string,   size: 'mini' | 'default' | 'large',   data: IUser }>(), {   title: '标题',   size: 'default',   data: () => {     return {name: '细狗', age: 18}   } }) 

注意:引用数据类型 data给默认值时需要通过一个构造方法进行返回,并且给默认值后,原来在定义类型时是必传参,也会自动转化为可选参。

4.父组件传入泛型声明

有时候我们不确定 props的参数类型,希望父组件通过传入泛型的方式进行类型定义,这时候我们可以使用 

这时,我们在父组件中传递参数时,就能够自动推导出参数对应的类型了:

ts代码解读复制代码   import Item from './Item.vue' import { ref } from 'vue'  import type { IUser, IDog } from './types.ts'  const user = ref({name: '细狗', age: 18}) const dog = ref({name: '舔狗',color: '白色'}) 

二、给组件定义的emits事件标记类型

1.运行时声明emits

ts代码解读复制代码 const emit = defineEmits(['change', 'confirm']) emit  // emit: (event: "change" | "confirm", ...args: any[]) => void 

这种声明方式比较方便,但是无法给触发的事件参数定义类型。如有这个需求,我们可以通过对象字面量的方式解决这个问题。

ts代码解读复制代码// 基于选项 const emit = defineEmits({   change: (id: number) => {     // 返回 `true` 或 `false`     // 表明验证通过或失败     if(id !== 1) return false     return true   },   confirm: (_value: string) => {     //value前面加个_,表示方法内可以忽略使用     return true   } }) // emit  // emit: ((event: "change", id: number) => void) & ((event: "confirm", value: string) => void) emit('change', 2) // 参数不是1,发出警告[Vue warn]: Invalid event arguments: event validation failed for event "change". emit('confirm', 'a') 

2.基于类型声明emits

使用这种方式可以方便的定义多个参数类型和事件返回值类型,也是目前来看比较常见的一种方式。

ts代码解读复制代码const emit = defineEmits<{   // 无返回值   (e: 'change', id: number, value: string): void   (e: 'confirm', value: string): void }>()

除此之外, vue3.3+,也新出了一种更简洁的语法,当你不关注事件返回值类型时,可以考虑这这种写法:

ts代码解读复制代码const emit = defineEmits<{   change: [id: number, value: string]   confirm: [value: string] }>()

这和前面声明的 emits事件是一样的,都是没有返回值。

三、给ref标记类型

1.使用泛型

ts代码解读复制代码// 默认值类型推导 const count = ref(0); // count: Ref // 和上面一个等价 const count = ref(0); // count: Ref // 不提供默认值时,类型推导会得到一个包含undefined的联合类型 const count = ref(); // count: Ref

2.Ref工具类型

ts代码解读复制代码const count: Ref = ref()  // 不能将类型“Ref”分配给类型“Ref” // ref()方法返回值类型必须和count定义的类型一致才行 const count: Ref = ref()

3.给reactive标注类型

ts代码解读复制代码interface IState {   name: string }  // state得到的类型推断为: {name: string} const state = reactive({   name: '细狗' })  // 显式声明类型,等价于上面的类型推导 const state: IState = reactive({   name: '细狗' })

四、给computed标记类型

ts代码解读复制代码const amount = ref(10) // 隐式推导返回值类型,doubleAmount: ComputedRef const doubleAmount = computed(() => amount.value * 2) // 显示定义返回值类型 const doubleAmount = computed(() => amount.value * 2)

五、给provide / inject 标注类型

ts代码解读复制代码/* 父组件 */ provide('name', 0)  /* 子组件 */ // name没有默认值 const myName = inject('name') // name: string | undefined // 给name提供默认值 const myName = inject('name', '哈哈') // name: string console.log(myName) // 0

我们上面再父组件中提供了一个 number类型的值,但是子组件中定义要注入的是 string类型的值,这样子是不会报错的!为了保证正确的类型,我们需要使用 Vue提供了一个  InjectionKey 接口,它是一个继承自  Symbol 的泛型类型,可以用来在提供者和消费者之间同步注入值的类型:

我们先建一个 constant.ts保存项目常量的文件,把 key定义在里面,这样子就可以父组件和子组件同时引入这个 key使用了:

ts代码解读复制代码/** constant.ts */ import type { InjectionKey  } from 'vue' export const name = Symbol() as InjectionKey  /** 父组件 */ import { name } from './constant' // provide(name, 0) // 类型“number”的参数不能赋给类型“string”的参数。 provide(name, '奥特曼')  /** 子组件 */ import { name } from './constant' const myName = inject(name, '哈哈') // name: string console.log(myName) // 奥特曼

六、给组件ref引用标记类型

当我们在自定义组件上绑定一个 ref值时,如果需要编辑器能够正确的提示组件内暴露的属性,则需要正确设置组件的 ref类型,下面来看一个例子

ts代码解读复制代码  import { ref } from 'vue'  const show = ref(false) const summit = () => {   console.log('提交') } const reset = () => {   console.log('重置') }  // 向外暴露属性或方法 defineExpose({   show,   summit,   reset }) 

ts代码解读复制代码    import Item from "./Item.vue"; import { ref} from "vue";  const ItemRef = ref(null) // ItemRef: Ref

以上没有给 ref传递泛型,所以使用 ItemRef时将得不到任何提示,因为是一个 null类型,如下图所示 image.png

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

ts代码解读复制代码 import Item from "./Item.vue"; import { ref} from "vue";  const ItemRef = ref | null>(null) 

这样我们就能得到正确的类型提示了!

image.png

如何你只希望读取组件的 $ref实例,或者所有组件都共享的属性,不关心其暴露值,也可以使用 vue内部提供的 ComponentPublicInstance类型

ts代码解读复制代码import type { ComponentPublicInstance } from 'vue' const ItemRef = ref(null)

image.png

总结

以上在定义组件 props类型时,分别提到了运行时声明和基于类型的声明的用法,在使用泛型声明类型时,如果需要给参数默认值,则需要使用 withDefaults编译宏,进行赋值;除此之外,还说明了如何定义通过父组件传递参数类型的组件。接着说明 emits事件也支持运行时类型和基于类型的定义方式,其中还提到了 vue3.3+新增的一种更为简洁的写法;接着我们简单介绍了 ref、reactive、computed的类型定义方式及一些注意事项;之后说明了在使用 provide/inject时,如何保证 提供和注入类型的一致性;最后在使用自定义组件时,通过 InstanceType的方式正确设置组件类型,以获得更好的类型提示支持。