I have, as I thought, a pretty simple task: render a React component according to its name/type.
Here is example of usage:
// WidgetsContainer.ts
// components have a difference in props shape!
const componentsData = [
{
type: 'WIDGET_1',
id: 'WIDGET_1',
title: 'Hello from widget 1',
someUniquePropForW1: true,
},
{
type: 'WIDGET_2',
id: 'WIDGET_2',
name: 'Jhonny',
someUniquePropForW2: 42,
}
]
return componentsData.map((componentData) => (
<ComponentRenderer key={componentData.id} {...componentData} />
));
So I've tried to implement my ComponentRenderer
in this way:
// Renderer.ts
import React from 'react';
interface IComponent<Properties = Record<string, unknown>> {
type: string;
id: string;
properties?: Properties;
}
const COMPONENT_TYPE = {
WIDGET_1: 'WIDGET_1',
WIDGET_2: 'WIDGET_2',
};
interface IWidget1Props {
title: string,
someUniquePropForW1: boolean,
}
interface IWidget2Props {
name: string,
someUniquePropForW2: number,
}
const W1Component = (props: IWidget1Props) => <h1>{props.title}</h1>
const W2Component = (props: IWidget2Props) => <h1>{props.name}</h1>
const componentMap = {
[COMPONENT_TYPE.WIDGET_1]: W1Component,
[COMPONENT_TYPE.WIDGET_2]: W2Component,
};
export const Renderer = ({ type, properties }: IComponent) => {
if (type in componentMap) {
const ComponentToRender = componentMap[type];
// !!!!!!!!!!!
// issue here!
// !!!!!!!!!!!
return React.createElement(ComponentToRender, properties);
}
return null;
};
Error looks like
Type 'PropsWithChildren<IWidget1Props>' is missing
the following properties from
type 'IWidget2Props': name, someUniquePropForW2
As I understand it, TS try to tell me something like that: "Hey! Here is situation possible when type is passed as WIDGET_1
, but props are passes as for W2Component
! I can't guarantee you are safe here".
So what I need is a way to tell the TS compiler that if type === WIDGET_1
props
always are IWidget1Props
.
I have been thinking about discriminated union
as a type guard but somehow I don't really want to put something like:
// Renderer.ts
// Some code here...
switch(type) {
case('WIDGET_1') return <W1Component {...properties}/>
case('WIDGET_2') return <W2Component {...properties}/>
//... 1000 more widgets
}
// Some code here...
Here is a link to TS sandbox with code described above.
How can I resolve this issue?
CodePudding user response:
This might solve your problem or can give you an idea
import React from 'react';
enum COMPONENT_TYPE {
WIDGET_1 = "WIDGET_1",
WIDGET_2 = "WIDGET_2"
}
interface IComponent<Properties = Record<string, unknown>> {
id: string;
type: COMPONENT_TYPE;
properties?: WIDGET_TYPE<Properties>;
}
// chain each widget type!
type WIDGET_TYPE<T = COMPONENT_TYPE> = T extends COMPONENT_TYPE.WIDGET_1 ? IWidget1Props : IWidget2Props
interface IWidget1Props {
title: string,
someUniquePropForW1: boolean,
}
interface IWidget2Props {
name: string,
someUniquePropForW2: number,
}
const W1Component = (props: IWidget1Props) => <h1>{props.title}</h1>
const W2Component = (props: IWidget2Props) => <h1>{props.name}</h1>
const componentMap = {
[COMPONENT_TYPE.WIDGET_1]: W1Component,
[COMPONENT_TYPE.WIDGET_2]: W2Component,
};
export const Renderer = ({ type, properties }: IComponent) => {
if (type in componentMap) {
const ComponentToRender = componentMap[type] as (props: WIDGET_TYPE<typeof type>) => JSX.Element;
return React.createElement(ComponentToRender, properties);
}
return null;
};
It's kinda hard to add type-safe in your situation. If the code block does not work for you, I do not have any other idea than casting ComponentToRender as any
const ComponentToRender = componentMap[type] as any
UPDATE:
Instead of chaining widget types you can use React.FC
casting:
const ComponentToRender = componentMap[type] as React.FC
CodePudding user response:
If you used the factory structure while creating the component, you would have solved the definition in the system from a single place. You can use the link below for the factory structure.
https://refactoring.guru/design-patterns/factory-method/typescript/example#example-0