人人都会AI编程

18.4 泛型组件、高阶组件类型封装

更新时间:2026-07-09

在 TypeScript 与 Vue 结合的项目中,泛型组件和高阶组件能让代码的复用性和类型安全性再上一个台阶——前者让一个组件可以处理不同类型的数据结构,后者则是对组件逻辑的抽象封装。本节围绕这两个主题,给出最实用的类型定义方式。


泛型组件

泛型组件最常见的场景是列表渲染:一个列表组件需要接收任意类型的数据,并通过插槽暴露每一项的实例,同时保持严格的类型检查。

Vue 3.3+ 为 <script setup> 提供了 generic 属性来声明泛型参数:

<!-- GenericList.vue -->
<script setup lang="ts" generic="T">
defineProps<{
  items: T[]
}>()

defineEmits<{
  select: [item: T]
}>()
</script>

<template>
  <ul>
    <li v-for="(item, index) in items" :key="index" @click="$emit('select', item)">
      <!-- 默认插槽,对外暴露 item -->
      <slot :item="item" />
    </li>
  </ul>
</template>

使用该组件时,TypeScript 会根据传入的 items 自动推导出 T 的具体类型:

<script setup lang="ts">
import GenericList from './GenericList.vue'

interface User {
  id: number
  name: string
}

const users: User[] = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' }
]

function handleSelect(user: User) {
  console.log(user.name) // user 的类型被正确推断
}
</script>

<template>
  <GenericList :items="users" @select="handleSelect">
    <!-- 作用域插槽中的 item 被推断为 User -->
    <template #default="{ item }">
      <span>{{ item.name }}</span>
    </template>
  </GenericList>
</template>

如果项目还停留在 Vue 3.2 或更低版本,泛型组件可以通过 defineComponent 显式声明:

// GenericList.ts
import { defineComponent, PropType } from 'vue'

export default defineComponent({
  props: {
    items: { type: Array as PropType<any[]>, required: true }
  },
  emits: ['select'],
  setup(props, { emit }) {
    // 由于 props.items 被声明为 any[],这里失去了精确类型
    // 但可以在使用处通过类型断言或传递泛型参数补救
  }
})

这种方式无法做到使用处的自动推导,不如 generic 属性优雅。因此,如果你的技术栈允许,建议升级到 Vue 3.3+ 并使用 <script setup generic>


高阶组件类型封装

“高阶组件”(Higher-Order Component, HOC)在 React 中极为常见,在 Vue 中通常被组合式函数(Hooks) 替代,但有时仍需要返回一个包装过的组件以复用模板逻辑(例如公共的加载态、错误边界或权限校验)。这时我们需要确保类型安全——被包装的组件所接收的 props 和 emits 能够被正确透传和增强。

下面是一个典型的 withLoading 高阶组件,它接受任意组件,返回一个带有 loading 属性的新组件:

// withLoading.ts
import { h, defineComponent, PropType, Component } from 'vue'

/**
 * 高阶组件:为传入的组件增加 loading 状态
 * @param WrappedComponent 需要包装的组件
 * @returns 新组件,具有 loading prop 和原组件的所有 props
 */
export function withLoading<T extends Component>(
  WrappedComponent: T
) {
  return defineComponent({
    props: {
      // 将原组件的 props 透传(使用 any 来泛化)
      wrappedProps: {
        type: Object as PropType<Record<string, any>>,
        default: () => ({})
      },
      loading: {
        type: Boolean,
        default: false
      }
    },
    setup(props, { attrs }) {
      // 返回渲染函数
      return () => {
        if (props.loading) {
          return h('div', { class: 'loading' }, '加载中...')
        }
        // 渲染原始组件,将 wrappedProps 和外部 attrs 都传递下去
        return h(WrappedComponent, {
          ...props.wrappedProps,
          ...attrs
        })
      }
    }
  })
}

使用示例

<script setup lang="ts">
import UserProfile from './UserProfile.vue' // 假设需要 userId 和 showAvatar
import { withLoading } from './withLoading'

const UserProfileWithLoading = withLoading(UserProfile)

const userId = ref(1)
const isLoading = ref(false)
</script>

<template>
  <UserProfileWithLoading
    :loading="isLoading"
    :wrapped-props="{ userId, showAvatar: true }"
  />
</template>

类型封装的改进:上述示例中,wrappedProps 被声明为 Record<string, any>,失去了被包装组件的具体属性类型。如果希望保留准确的 props 类型提示,可以借助泛型推导:

// 更严格的类型封装
import { DefineComponent, ComponentPropsOptions } from 'vue'

export function withLoading<T extends Record<string, any>>(
  WrappedComponent: DefineComponent<T>
) {
  return defineComponent({
    props: {
      // 继承原组件的 props 定义(运行时 + 类型)
      ...(WrappedComponent.props as ComponentPropsOptions),
      loading: Boolean
    },
    setup(props, { attrs }) {
      return () => {
        if (props.loading) return h('div', '加载中...')
        // 直接透传当前组件收到的所有 props
        return h(WrappedComponent, { ...props, ...attrs })
      }
    }
  })
}

由于 Vue 组件的 props 类型推断较为复杂,在实际业务中,如果高阶组件的主要目的是逻辑复用,优先考虑组合式函数。例如上面的 loading 状态完全可以用一个 useRequest 的 hook 管理,而无需付出类型包装的高成本。高阶组件更适合必须复用模板片段(例如包裹固定 DOM 结构)且不想引入插槽的场景。


小结

  • 泛型组件使用 <script setup generic="T"> 可以轻松处理列表类、选择器类等接收任意数据类型的组件,是 Vue 3.3 以上最推荐的写法。
  • 高阶组件在 Vue 中应谨慎使用,类型封装比较复杂,通常可以用组合式 API 替代。确需使用时,建议保持类型宽松或显式继承 props,避免过度工程化。
  • 实用原则:优先用 Hooks 解决逻辑复用,用泛型组件解决数据类型泛化——这样既能享受类型安全,又不会让代码过度抽象。