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

Vue3+TSX中使用defineComponent定义泛型组件的更佳方案

Vue3 泛型组件在defineComponent下的更优实现方案

问题背景

在Vue3函数式组件模式下,可轻松定义泛型组件,示例代码如下:

type Loading = { status: 'LOADING' }
type Error = { status: 'ERROR'; message: string }
type AsyncData<T> = Loading | { status: 'LOADED'; value: T } | Error

type Props<T> = {
  data: AsyncData<T>
  renderItem: (item: T) => JSX.Element | null
}

const LoadingElement = <T,>(props: Props<T>) => {
  return (
    <div class={`${props.data.status === 'LOADING' ? styles['top-level'] : ''}`} style={props.data.status !== 'LOADED' ? 'display: flex; justify-content: center' : ''}>
      {props.data.status === 'LOADED' ? props.renderItem(props.data.value) : null}
      {props.data.status === 'ERROR' ? (
        <div>
          <HelpTooltip icon="error-warning" text={props.data.message} />
        </div>
      ) : null}
      {props.data.status === 'LOADING' ? <div class={styles.spinner}></div> : null}
    </div>
  )
}

使用该组件时,item可获得正确的类型提示。

但当使用defineComponent(需用到ref、watch、computed等API)时,无法直接为组件定义泛型。此前采用的hack方案较为繁琐:

const GenericComponentHack = <T>() =>
  defineComponent<Props<T>>({ setup(props) => {
    return () => <OtherComponent {...props} />
  })

export const GenericComponent = <T>(props: Props<T>): JSX.Element => {
  const GenericComponentHackInstance = GenericComponentHack<T>()
  // @ts-ignore
  return <GenericComponentHackInstance {...props} />
}

更优实现方式

方案1:函数包裹defineComponent(无需@ts-ignore)

直接返回带泛型参数的组件函数,避免额外实例化和类型忽略:

export const GenericComponent = defineComponent(<T>() => ({
  props: {
    data: {
      type: Object as () => AsyncData<T>,
      required: true
    },
    renderItem: {
      type: Function as () => (item: T) => JSX.Element | null,
      required: true
    }
  },
  setup(props) {
    // 可正常使用ref、watch、computed等响应式API
    const someRef = ref(null)
    watch(() => props.data, (newData) => {
      // 处理数据变化逻辑
    })
    
    return () => (
      <div>
        {props.data.status === 'LOADED' ? props.renderItem(props.data.value) : null}
        {/* 其余渲染逻辑 */}
      </div>
    )
  }
}))

方案2:Vue 3.3+ 原生支持defineProps泛型

Vue 3.3及以上版本中,defineProps原生支持泛型参数,无需额外hack:

import { defineComponent, defineProps, ref, watch } from 'vue'

export const GenericComponent = <T>() => defineComponent({
  setup() {
    const props = defineProps<Props<T>>()
    
    // 正常使用响应式API
    const count = ref(0)
    watch(() => props.data, () => {
      count.value++
    })
    
    return () => (
      <div>
        {props.data.status === 'LOADED' ? props.renderItem(props.data.value) : null}
        {/* 其余渲染逻辑 */}
      </div>
    )
  }
})

使用组件时可显式传递泛型:

<GenericComponent<MyDataType> data={myData} renderItem={(item) => <div>{item.name}</div>} />

方案3:带响应式逻辑的函数式组件

若组件内部状态无需持久化,可直接给函数式组件添加泛型,同时在内部使用响应式API:

export const GenericComponent = <T>(props: Props<T>) => {
  // 注意:此处ref、computed等会在每次渲染时创建新实例,仅适用于无持久化状态的场景
  const isLoading = computed(() => props.data.status === 'LOADING')
  
  return () => (
    <div class={`${isLoading.value ? styles['top-level'] : ''}`}>
      {props.data.status === 'LOADED' ? props.renderItem(props.data.value) : null}
      {/* 其余渲染逻辑 */}
    </div>
  )
}

注:若组件需要持久化内部状态,优先选择前两种结合defineComponent的方案。

内容的提问来源于stack exchange,提问作者michael_bitard

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 13:29:59