Neminda Prabhashwara
Neminda Prabhashwara

Reputation: 434

How to define the type of a prop, which is a functional component while specifying the FC's various prop types

I have two functional components which have two different prop types.

export interface OrderImageProps {
    orders: ICartItem[]
}

const OrderImage: React.FC<OrderImageProps> = (props: OrderImageProps) => {
    return (
        <div>
            // show some list
        </div>
    );
};

and

export interface ProductImageProps {
    src: string
}

const ProductImage: React.FC<ProductImageProps> = (props: ProductImageProps) => {
    return (
        <Image className='img-fluid admin-product-image' src={props.src}/>
    );
};

These two components are entities of a table, but conditionally rendered. So I need to pass these two components to the table component.

interface CustomDataTableProps {
    nameTag: string
    itemTag: string
    priceTag: string
    categoryTag: string
    dateTag: string
    actionsTag: string
    image: React.FC<ProductImageProps | OrderImageProps>
    list: boolean
}
    
const CustomDataTable: React.FC<CustomDataTableProps> = (props: CustomDataTableProps) => {
    const cartItems: ICartItem[] = useSelector((state: RootState): ICartItem[] => (state.cart.cartItems));
    const sampleData = sampleProducts.map((product: ICartItem) => {

    return {
        image: React.createElement(props.image, props.list ?
        {
            orders: cartItems
        } :
        {
            src: product.src
        }),
        // more data are in here
    };
}

But when passing props to the CustomDataTable, it shows FC<ProductImageProps> is not assignable to type FC<ProductImageProps | OrderImageProps>.

I understand this error, clearly they are incompatible types.

From my understanding may be these props need an XOR behavior on React.FC<> type.

This can easily be done by providing a render prop on CustomDataTable. Also there are other table entities which have like this kind of conditionally rendered components.

Can my first approach be achieved or is it better if I use a render prop?

Upvotes: 0

Views: 121

Answers (1)

aleksxor
aleksxor

Reputation: 8380

React.FC is essentially a function type with extra steps. And type parameter P in React.FC<P> goes in negative/contravariant position.

When you try to pass image prop into CustomDataTable it checks function compatibility and when it comes to arguments it check them in reverse direction. When you pass it an argument with type React.FC<ProductImageProps> it doesn't check whether ProductImageProps is assignable to ProductImageProps | OrderImageProps but whether ProductImageProps | OrderImageProps is assignable to ProductImageProps. That it's clearly not.

Changing your image prop type to React.FC<ProductImageProps> | React.FC<OrderImageProps> will get rid of previous ... is not assignable ... error. But you'll get another error at React.createElement call site.

Now your new props.image has type React.FC<ProductImageProps> | React.FC<OrderImageProps>. But this type accepts only arguments of type ProductImageProps & OrderImageProps. i.e. having all required fields from both your prop types.

type PropsA = { a: string }
type PropsB = { b: string }

type A = (propsA: PropsA) => void
type B = (propsB: PropsB) => void

type AB = A | B

function fnFromAB(ab: AB): void { // ab has type (args: PropsA & PropsB) => void
    ab({ a: 'string' }) // error
    ab({ b: 'string' }) // error
    ab({ a: 'string ', b: 'string' }) // no error
}

playground link

The solution is to make type of image component depend on type of it's props. And since type of props depends on list flag you may rewrite your code as:

type CustomDataTableProps = {
    nameTag: string
    itemTag: string
    priceTag: string
    categoryTag: string
    dateTag: string
    actionsTag: string
} & ({
    image: React.FC<OrderImageProps>
    list: true
} | {    
    image: React.FC<ProductImageProps>
    list: false
})
    
const CustomDataTable: React.FC<CustomDataTableProps> = (props: CustomDataTableProps) => {
    const cartItems: ICartItem[] = useSelector((state: RootState): ICartItem[] => (state.cart.cartItems));
    const sampleData = sampleProducts.map((product: ICartItem) => {

    const image = props.list
        ? React.createElement(props.image, { orders: cartItems })
        : React.createElement(props.image, { src: product.src })

    return {
        image,
        // more data are in here
    };
}

Unfortunately you'll have to duplicate React.createElement call since props.image has different types in each of the arms of ternary operator.

You may cut some corners with type assertions ofcourse. But for the cost of type safety. As usual.

Upvotes: 1

Related Questions