You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.08 09:15:31