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

TypeScript下如何正确包装React多态组件并保留类型?

解决React泛型组件包装的TypeScript类型错误

问题场景

我实现了一个支持动态HTML标签的泛型Text组件,现在需要封装一个Display包装组件,预设Text的group属性为display,同时保留传递任意原生HTML元素属性的能力。现有代码如下:

import { ElementType, ComponentPropsWithRef, ReactNode } from 'react';

interface BaseTextProps<T extends ElementType> {
  tag?: T;
  size: '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800';
  children: ReactNode;
}

type TextProps<T extends ElementType> = BaseTextProps<T> & {
  group: 'display' | 'code';
}

function Text<T extends ElementType = 'span'>({
  tag,
  group,
  size,
  children,
  ...rest
}: TextProps<T> & Omit<ComponentPropsWithRef<T>, keyof TextProps<T>>) {
  const Component = tag ?? 'span';
  return <Component {...rest}>{children}</Component>;
}

type DisplayProps<T extends ElementType> = BaseTextProps<T> & {
  size: '100' | '200' | '300';
}

export function Display<T extends ElementType>({
  tag,
  size,
  children,
}: DisplayProps<T>) {
  return (
    <Text group="display" size={size} tag={tag}>
      {children}
    </Text>
  );
}

单独使用Text组件时类型检查正常:

function Name() {
    return (
        <Text tag="p" size="100" group="display">Hello</Text>
    )
}

但Display组件会触发TypeScript错误:

Type '{ children: ReactNode; group: "display"; tag: T | undefined; size: 
"100" | "200" | "300" | "400" | "500"; }' is not assignable to type 
'Omit<ComponentPropsWithRef<T>, keyof TextProps<T>>'. ts(2322)

奇怪的是,改用createElement语法却能通过检查:

return createElement(Text, { group: "display", size, tag, children })

问题原因

TypeScript对JSX语法下的泛型组件类型推断存在特殊逻辑:当组件props是交叉类型(TextProps<T> & Omit<...>)时,JSX会尝试拆分交叉类型进行匹配,错误地将传入的props和Omit部分类型做校验,而非使用完整的交叉类型。而createElement是直接传递props对象,类型检查逻辑更直接,不会触发这个推断偏差。

解决方案

方案1:完善Display的props定义并透传原生属性

调整DisplayProps,加入原生HTML元素的剩余属性,让Display能接收并透传这些属性,同时修复类型匹配问题:

import { ElementType, ComponentPropsWithRef, ReactNode } from 'react';

interface BaseTextProps<T extends ElementType> {
  tag?: T;
  size: '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800';
  children: ReactNode;
}

type TextProps<T extends ElementType> = BaseTextProps<T> & {
  group: 'display' | 'code';
}

function Text<T extends ElementType = 'span'>({
  tag,
  group,
  size,
  children,
  ...rest
}: TextProps<T> & Omit<ComponentPropsWithRef<T>, keyof TextProps<T>>) {
  const Component = tag ?? 'span';
  return <Component {...rest}>{children}</Component>;
}

// 新增原生元素属性的支持
type DisplayProps<T extends ElementType> = BaseTextProps<T> & 
  { size: '100' | '200' | '300' } &
  Omit<ComponentPropsWithRef<T>, keyof BaseTextProps<T>>;

export function Display<T extends ElementType>({
  tag,
  size,
  children,
  ...rest
}: DisplayProps<T>) {
  return (
    <Text 
      group="display" 
      size={size} 
      tag={tag}
      {...rest}
    >
      {children}
    </Text>
  );
}

这个方案不仅修复了类型错误,还让Display组件支持传递原生HTML属性(如className、id),更符合组件封装的预期。

方案2:显式指定Text的泛型参数

在JSX中显式传入Text组件的泛型参数,让TypeScript明确类型匹配关系:

export function Display<T extends ElementType>({
  tag,
  size,
  children,
}: DisplayProps<T>) {
  return (
    <Text<T> group="display" size={size} tag={tag}>
      {children}
    </Text>
  );
}

这种方式更简洁,但没有支持原生属性透传,适合不需要额外传递原生属性的场景。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 04:55:54