如何为React TypeScript组件数组中每个对象单独推断data类型?
问题:为React TypeScript组件的数组元素单独推断data属性类型
我有一个接收数组作为输入的React TypeScript组件,该组件通过遍历数组将每个对象渲染为Card组件。数组中的每个对象包含多个属性,其中data属性的类型在不同对象中可能存在差异。
当向该组件传入包含不同data类型对象的数组时,TypeScript编译器会根据数组中的第一个对象推断data的类型,这会导致渲染后续具有不同data类型的对象时出现类型错误。
以下是Card组件及ListPrintingPoints组件的示例代码:
import { forwardRef, ReactNode, HTMLAttributes, Ref, ForwardedRef, ElementType, } from 'react'; export type Tabulated<T> = { header?: string; data: T; dataContainer?: ElementType; formatter?: (data: T) => ReactNode; isVisible?: boolean; }; export interface CardProps<T> extends HTMLAttributes<HTMLElement> { readonly tabulated: Tabulated<T>[]; readonly className?: string; } const CardContainer = <T,>( { tabulated, className = '', ...rest }: CardProps<T>, ref: Ref<HTMLElement> ) => { return ( <article className={`flex flex-col sm:flex-row bg-white rounded-md p-4 gap-2 ${className}`} ref={ref} {...rest} > {tabulated.map( ({ header, data, dataContainer: Container = 'p', formatter, isVisible = true, }) => ( <> {isVisible && ( <div className={`${header ? 'flex-1' : 'w-fit'}`}> {header && ( <h3 className="text-sm sm:text-base leading-6 font-normal text-gray-500 mb-1"> {header} </h3> )} <Container className={`${ Container === 'p' ? 'text-sm sm:text-base leading-normal font-bold text-gray-700 w-full' : 'w-full' } ${!header ? 'h-full' : ''} `} > {!data && !formatter && '-'} {data && !formatter && (data as ReactNode)} {data && formatter && formatter(data)} </Container> </div> )} </> ) )} </article> ); }; export const Card = forwardRef(CardContainer) as <T>( props: CardProps<T> & { ref?: ForwardedRef<HTMLElement>; } ) => ReturnType<typeof CardContainer>; type PrintingPoint = { serialCode: string; macs: { ethMac: string; wlanMac: string; } } const printingPoints: PrintingPoint[] = [ { serialCode: 'oDYxPieXt98mPuDVp2GF', macs: { ethMac: 'B7:68:83:5E:B7:27', wlanMac: '6A:AF:90:44:92:9E' } }, { serialCode: 'MjGxwY9uEgHvtq78HHsH', macs: { ethMac: '9A:66:00:F2:B9:03', wlanMac: '0D:C3:75:1B:4C:A6' } } ] interface ListPrintingPointsProps { printingPoints: PrintingPoint[] } const ListPrintingPoints = ({ printingPoints }: ListPrintingPointsProps) => { return ( <ul> {printingPoints.map((printingPoint) => ( <li key={printingPoint.serialCode}> <Card tabulated={[ { header: 'Serial Code', data: printingPoint.serialCode, }, { header: 'Ethernet:MAC', data: printingPoint.macs.ethMac, formatter: (data) => data.toUpperCase(), }, { header: 'WLAN:MAC', data: printingPoint.macs.wlanMac, formatter: (data) => data.toUpperCase(), }, { dataContainer: 'div', data: ( <div className="flex justify-end items-center w-full h-full"> <button> <ArrowRightIcon className="stroke-2 text-brand-500 w-5 h-5" /> </button> </div> ), }, ]} /> </li> ))} </ul> ) }
请问是否可以为数组中的每个对象单独推断data属性的类型,以避免上述类型错误问题?
解决方案
问题根源在于原组件用单一泛型T约束整个tabulated数组的data类型,导致所有元素的data必须是同一类型,无法支持不同类型的data。我们可以通过修改类型定义,让数组中每个元素的data类型独立,同时保证formatter与对应data的类型匹配。
具体修改步骤
1. 调整类型定义,支持独立的元素类型
将CardProps的单一泛型约束改为接受包含任意Tabulated元素的数组,每个元素可以拥有自己的data类型:
export type Tabulated<T> = { header?: string; data: T; dataContainer?: ElementType; formatter?: (data: T) => ReactNode; isVisible?: boolean; }; // 移除单一泛型T,改为接受任意Tabulated元素的数组 export interface CardProps extends HTMLAttributes<HTMLElement> { readonly tabulated: readonly Tabulated<unknown>[]; readonly className?: string; }
2. 更新组件和forwardRef的类型声明
去掉组件的单一泛型T,改为支持任意Tabulated元素的数组:
const CardContainer = ( { tabulated, className = '', ...rest }: CardProps, ref: Ref<HTMLElement> ) => { // 原有组件逻辑保持不变 return ( <article className={`flex flex-col sm:flex-row bg-white rounded-md p-4 gap-2 ${className}`} ref={ref} {...rest} > {tabulated.map( ({ header, data, dataContainer: Container = 'p', formatter, isVisible = true, }) => ( <> {isVisible && ( <div className={`${header ? 'flex-1' : 'w-fit'}`}> {header && ( <h3 className="text-sm sm:text-base leading-6 font-normal text-gray-500 mb-1"> {header} </h3> )} <Container className={`${ Container === 'p' ? 'text-sm sm:text-base leading-normal font-bold text-gray-700 w-full' : 'w-full' } ${!header ? 'h-full' : ''} `} > {!data && !formatter && '-'} {data && !formatter && (data as ReactNode)} {data && formatter && formatter(data)} </Container> </div> )} </> ) )} </article> ); }; export const Card = forwardRef(CardContainer) as ( props: CardProps & { ref?: ForwardedRef<HTMLElement> } ) => ReturnType<typeof CardContainer>;
3. 更严谨的类型约束(可选)
如果需要保留每个元素的类型信息,让TypeScript能准确检查formatter的参数类型,可以使用泛型数组:
// 用泛型数组定义CardProps,保留每个元素的类型信息 export type CardProps<T extends readonly Tabulated<any>[]> = HTMLAttributes<HTMLElement> & { readonly tabulated: T; readonly className?: string; }; const CardContainer = <T extends readonly Tabulated<any>[]>( { tabulated, className = '', ...rest }: CardProps<T>, ref: Ref<HTMLElement> ) => { // 原有组件逻辑不变 }; export const Card = forwardRef(CardContainer) as <T extends readonly Tabulated<any>[]>( props: CardProps<T> & { ref?: ForwardedRef<HTMLElement> } ) => ReturnType<typeof CardContainer>;
效果验证
修改后,传入包含不同data类型的tabulated数组时,TypeScript会为每个元素单独推断data的类型:
- 前三个元素的
data为string类型,formatter的参数会被正确推断为string,支持toUpperCase()调用; - 最后一个元素的
data为ReactNode类型,也能正常通过类型检查。
内容的提问来源于stack exchange,提问作者Leonardo Moraes
相关产品推荐
相关产品推荐

