前言
vue3
已经出来三年多了,其中新增了组合式api
,并且也提供了更好的TypeScript
类型标记支持,之后还推出了<script setup>
语法糖,使我们的开发体验进一步提升。不知道大家在使用vue3
配合ts
开发时,是否能够正确标记对应的类型,以获得更好的类型提升支持。下面精心 整理了一篇使用vue3 + typesctypt + <script setup>
开发时,如何正确进行类型标记,包括最新的api
使用方法,带你快速上手vue3 + ts
开发。
一、给组件的props参数定义类型
1.运行时声明
运行时声明是在代码运行时才会确定参数的类型,在编译阶段不会进行类型检测。
1.1 运行时-简单类型声明
js
<script setup lang="ts">
const props = defineProps({
title: String,
size: {
type: String,
required: true,
validate(val: string) {
return ['mini', 'default', 'large'].includes(val)
}
}
})
props.title // title?: string | undefine
props.size // size: string
</script>
这种声明方式基本和选项式api的声明方式一致。
1.2 运行时-复杂类型声明
对于运行时声明,有时候我们需要给一个props
参数声明为一个自定义接口类型 ,这时需要借助vue
内部提供的PropType
工具类型进行类型断言:
ts
<script setup lang="ts">
import type { IUser } from './types.ts'
import type { PropType } from 'vue'
const props = defineProps({
data: {
type: Object as PropType<IUser>,
required: true
}
})
props.data // data: IUser
</script>
2.基于类型的声明
基于类型的声明在编译保存代码编译阶段就能确定类型,获得更好的类型提示。通过泛型参数来定义props
的类型比较常用,通过这种方式定义的类型看起来也更加直观。
ts
<script setup lang="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
</script>
以上我们通过泛型定义的参数和上面基于运行时声明参数是一样的,但是明显可以看出这样写简洁了许多。
3.withDefaults定义参数默认值
基于类型的声明在给参数赋默认值时,需要借助withDefaults
编译器宏来实现。
ts
<script setup lang="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}
}
})
</script>
注意 :引用数据类型data
给默认值时需要通过一个构造方法进行返回,并且给默认值后,原来在定义类型时是必传参,也会自动转化为可选参。
4.父组件传入泛型声明
有时候我们不确定props
的参数类型,希望父组件通过传入泛型的方式进行类型定义,这时候我们可以使用 <script>
标签上的 generic
属性声明泛型类型参数:
ts
<script setup lang="ts" generic="T, U extends Object">
const props = defineProps<{
title?: T,
data: U
}>()
props.title // title?: T | undefined
props.data // data: U extends Object
</script>
这时,我们在父组件中传递参数时,就能够自动推导出参数对应的类型了:
ts
<template>
<Item :data="user" title="人"></Item> <!-- data:IUser,title?: string | undefined -->
<Item :data="dog" :title="0"></Item> <!-- data:IDog,title?: number | undefined -->
</template>
<script setup lang="ts">
import Item from './Item.vue'
import { ref } from 'vue'
import type { IUser, IDog } from './types.ts'
const user = ref<IUser>({name: '细狗', age: 18})
const dog = ref<IDog>({name: '舔狗',color: '白色'})
</script>
二、给组件定义的emits事件标记类型
1.运行时声明emits
ts
<script lang="ts" setup>
const emit = defineEmits(['change', 'confirm'])
emit // emit: (event: "change" | "confirm", ...args: any[]) => void
</script>
这种声明方式比较方便,但是无法给触发的事件参数定义类型。如有这个需求,我们可以通过对象字面量的方式解决这个问题。
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')
</script>
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<number>
// 和上面一个等价
const count = ref<number>(0); // count: Ref<number>
// 不提供默认值时,类型推导会得到一个包含undefined的联合类型
const count = ref<number>(); // count: Ref<number | undefined>
2.Ref工具类型
ts
const count: Ref<number> = ref() // 不能将类型"Ref<number | undefined>"分配给类型"Ref<number>"
// ref()方法返回值类型必须和count定义的类型一致才行
const count: Ref<number | undefined> = 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<number>
const doubleAmount = computed(() => amount.value * 2)
// 显示定义返回值类型
const doubleAmount = computed<number>(() => amount.value * 2)
五、给provide / inject 标注类型
ts
/* 父组件 */
provide('name', 0)
/* 子组件 */
// name没有默认值
const myName = inject<string>('name') // name: string | undefined
// 给name提供默认值
const myName = inject<string>('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<string>
/** 父组件 */
import { name } from './constant'
// provide(name, 0) // 类型"number"的参数不能赋给类型"string"的参数。
provide(name, '奥特曼')
/** 子组件 */
import { name } from './constant'
const myName = inject<string>(name, '哈哈') // name: string
console.log(myName) // 奥特曼
六、给组件ref引用标记类型
当我们在自定义组件上绑定一个ref
值时,如果需要编辑器能够正确的提示组件内暴露的属性,则需要正确设置组件的ref
类型,下面来看一个例子
ts
<!-- Item.vue -->
<script lang="ts" setup>
import { ref } from 'vue'
const show = ref(false)
const summit = () => {
console.log('提交')
}
const reset = () => {
console.log('重置')
}
// 向外暴露属性或方法
defineExpose({
show,
summit,
reset
})
</script>
ts
<!-- Parent.vue -->
<template>
<Item ref="Item"></Item>
</template>
<script setup lang="ts">
import Item from "./Item.vue";
import { ref} from "vue";
const ItemRef = ref(null) // ItemRef: Ref<null>
以上没有给ref
传递泛型,所以使用ItemRef
时将得不到任何提示,因为是一个null
类型,如下图所示
为了得到Item
的类型,我们首先需要通过TypeScript
中的typeof
得到Item
组件的类型,再使用TypeScript
内置的InstanceType
工具类型来获取其实例类型:
ts
<script setup lang="ts">
import Item from "./Item.vue";
import { ref} from "vue";
const ItemRef = ref<InstanceType<typeof Item> | null>(null)
</script>
这样我们就能得到正确的类型提示了!
如何你只希望读取组件的$ref
实例,或者所有组件都共享的属性,不关心其暴露值,也可以使用vue
内部提供的ComponentPublicInstance
类型
ts
import type { ComponentPublicInstance } from 'vue'
const ItemRef = ref<ComponentPublicInstance | null>(null)
总结
以上在定义组件props
类型时,分别提到了运行时声明和基于类型的声明的用法,在使用泛型声明类型时,如果需要给参数默认值,则需要使用withDefaults
编译宏,进行赋值;除此之外,还说明了如何定义通过父组件传递参数类型的组件。接着说明emits
事件也支持运行时类型和基于类型的定义方式,其中还提到了vue3.3+
新增的一种更为简洁的写法;接着我们简单介绍了ref、reactive、computed
的类型定义方式及一些注意事项;之后说明了在使用provide/inject
时,如何保证提供和注入类型的一致性 ;最后在使用自定义组件时,通过InstanceType<typeof 组件>
的方式正确设置组件类型,以获得更好的类型提示支持。