Vue.js 3中TypeScript类型安全封装Vuetify组件的问题咨询
Vue.js 3中TypeScript类型安全封装Vuetify组件的问题咨询
最近在Vue 3 + TypeScript项目里尝试用包装器组件模式封装Vuetify组件,过程中遇到了不少类型相关的棘手问题,想跟大家分享下我的实现思路和目前卡壳的地方。
首先,包装器组件的核心是要透传被包装组件的props、事件和插槽,同时保持TypeScript的类型安全。如果不考虑类型的话,用v-bind="$attrs"就能轻松搞定,但$attrs只包含未在defineProps里声明的属性/监听器,要类型安全就必须手动声明props,这就意味着我们得分别处理props、emits和slots的类型,还要手动转发它们:
- Props:用
v-bind绑定 - Emits:用
v-on绑定 - Slots:通过循环转发所有插槽,代码大概是这样:
<template v-for="(_, slot) of slots" #[slot]="scope"> <slot :name="slot" v-bind="scope" /> </template>
看起来简单?实际写起来才发现水很深,我最终的实现代码是这样的:
类型定义与组件逻辑
import { VCard, VDialog } from 'vuetify/components' // Vue保留的关键字,会触发lint错误 type VueReservedKeys = | '$children' | 'key' | 'ref' | 'ref_for' | 'ref_key' | `on${string}` // JSX事件 | `v-slot${string}` // Vue插槽内部属性 export type CleanProps<T> = Omit<T, VueReservedKeys> // 提取VDialog的props、emits、slots类型 type VDialogProps = InstanceType<typeof VDialog>['$props'] type VDialogEmits = Exclude<(typeof VDialog)['emits'], undefined> type VDialogSlots = InstanceType<typeof VDialog>['$slots'] // 提取VCard的slots类型 type VCardSlots = InstanceType<typeof VCard>['$slots'] const props = withDefaults( defineProps<CleanProps<VDialogProps> & { icon?: string; title: string; description: string }>(), { persistent: true } ) const emit = defineEmits<VDialogEmits>() const slots = defineSlots<VDialogSlots & Pick<VCardSlots, 'actions'>>() function getSlotKeys<S extends object>(slots: S): Array<keyof S> { // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Object.keys的类型比较宽松 return Object.keys(slots) as Array<keyof S> }
组件模板
<v-dialog v-bind="props" v-on="emit"> <template #default="scope"> <slot v-if="getSlotKeys(slots).includes('default')" v-bind="scope" /> <v-card v-else class="pa-4" rounded="xl"> <div class="d-flex justify-end"> <v-btn icon="mdi-close" variant="text" @click="scope.isActive.value = !scope.isActive.value" /> </div> <v-card-title class="d-flex align-center justify-center pt-0 text-h5 font-weight-bold"> <v-icon v-if="icon" class="mr-2" color="black">{{ icon }}</v-icon> {{ title }} </v-card-title> <v-card-text class="text-center text-body-1 mt-2"> {{ description }} </v-card-text> <v-card-actions class="justify-center pa-6 ga-4"> <slot name="actions" /> </v-card-actions> </v-card> </template> <template v-for="slot of getSlotKeys(slots).filter( (key): key is Exclude<typeof key, 'actions' | 'default'> => !['actions', 'default'].includes(key) )" #[slot]="scope" > <slot :name="slot" v-bind="scope" /> </template> </v-dialog>
遇到的问题
看起来逻辑通顺,但实际开发时,vue-language-tools(Vue的语言服务插件)根本处理不了这么复杂的类型,直接报了两个错误:
- Props类型报错:
[plugin:vite:vue] Type parameter list cannot be empty. (99:33) - Emits类型报错:
[@vue/compiler-sfc] Unresolvable type reference or unsupported built-in utility type
目前我能想到的临时解决方案是:如果Vuetify官方能直接导出组件的props和emits类型,这个问题会简单很多,但这完全依赖组件开发者的实现,不是我们能控制的。
想请教下大家,有没有更优雅的方法来实现TypeScript类型安全的Vuetify组件包装器?
内容来源于stack exchange
相关产品推荐
相关产品推荐

