I currently have two components:
export interface MediaCardProps {
alt: string;
src: string;
}
export const MediaCard = ({ alt, src }: MediaCardProps) => {
...
};
and
export interface TextCardProps {
isLoaded: boolean;
children: ReactNode;
}
export const TextCard = ({ children, isLoaded = false }: TextCardProps) => {
...
};
I would like to create a parent component like this:
type Props = ({ isMedia: false } & TextCardProps) | ({ isMedia: true } & MediaCardProps);
export const Card = ({ isMedia = false, ...args }: Props) => {
if (isMedia) return <MediaCard {...(args as MediaCardProps)} />;
return <TextCard {...(args as TextCardProps)} />;
};
The goal here is that when isMedia
is set to true, we only see the MediaCardProps
, otherwise, we see the TextCardProps
.
But doing so, I have an issue when calling it. I call it this way:
<Card isLoaded={!!data.title}>
<h1 className='text-2xl font-bold'>{data.title}</h1>
</Card>
and I have this error Type '{ children: Element; isLoaded: boolean; }' is not assignable to type 'IntrinsicAttributes & Props'. Property 'isMedia' is missing in type '{ children: Element; isLoaded: boolean; }' but required in type '{ isMedia: false; }'.
I managed to fix it by changing my parent Props to this:
type Props = { isMedia?: false; args: TextCardProps } | { isMedia: true; args: MediaCardProps };
but I don't want the args prop.
Many thanks if you can help me fix this !
Your Card component needs to infer information about the requested media type. This can be achieved by defining the Card component as a generic component that takes CardType as a generic parameter.
The same logic applies to the boolean type, but for better flexibility, I replaced the boolean type with a union of types:
type CardType = 'media' | 'text'
Now, make the Card component generic:
const Card = <TCardType extends CardType>(props: Props<TCardType>) => { ... }
We pass TCardType
as a generic type to the Props
. To infer its actual value, we need to define a conditional logic inside our dynamic generic Props
type.
type Props<CardType> = { type?: CardType } & (CardType extends 'media' ? MediaCardProps : TextCardProps)
The { type?: CardType }
allows for the type to be optional (defaulting to 'text'
) and specifies the user-defined type. The type of this field will be inferred, allowing Props
to provide the proper structure for the actual Card
implementation (with isMedia
boolean, you would check by T extends true
instead).
Lastly, inside the generic Card
component, we only need to verify the kind of props we are dealing with. Based on this information, we're able to return the appropriate Card
implementation:
const Card = <TCardType extends CardType>(props: Props<TCardType>) => {
if (isMediaProps(props)) {
return <MediaCard {...props} />;
}
if (isTextProps(props)) {
return <TextCard {...props} />;
}
return null
};
Here's a TS playground for you with a complete solution.