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
相关产品推荐
相关产品推荐

