Фабрика компонентов React Typescript. Проблемы с типизацией
Моя задача динамически рендерить компоненты разных типов. Для начала я создал enum дабы как то обозначить компоненты:
enum ComponentTypes {
FOOTER = "FOOTER",
HEADER = "HEADER"
}
В этом примере есть всего лишь два типа компонента: Footer и Header. Каждый из этих компонентов будет принимать разные пропсы, поэтому, помимо самих компонентов, я создал и функции, который передадут извне нужные пропсы соответствующим компонентам:
interface IFooter {
info?: string[];
}
const Footer: React.FC<IFooter> = ({ info }) => {
return (
<div style={{ minHeight: "100px", background: "blue" }}>
{info && info.map((block) => <p>{block}</p>)}
</div>
);
};
const renderFooter = (data: IFooter): React.ReactNode => {
return <Footer {...data} />;
};
//===================================================================
interface IHeader {
login: string;
title: string;
}
const Header: React.FC<IHeader> = ({ login, title }) => {
return (
<div>
<h1>{login}</h1>
<span>{title}</span>
</div>
);
};
const renderHeader = (data: IHeader): React.ReactNode => {
return <Header {...data} />;
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
Теперь я создал таблицу (фабрику) компонентов по которой буду ориентироваться что рендерить:
interface ITable {
[ComponentTypes.HEADER]: (data: IHeader) => React.ReactNode;
[ComponentTypes.FOOTER]: (data: IFooter) => React.ReactNode;
}
const table: ITable = {
[ComponentTypes.HEADER]: renderHeader,
[ComponentTypes.FOOTER]: renderFooter
};
Так же я создал компонент, в котором объявил нужные данные и попытался воспользоваться таблицей выше:
interface IFooterData {
type: ComponentTypes.FOOTER;
data: IFooter;
}
interface IHeaderData {
type: ComponentTypes.HEADER;
data: IHeader;
}
export default function App() {
const data: (IFooterData | IHeaderData)[] = [
{
type: ComponentTypes.HEADER,
data: {
login: "Login",
title: "Title"
}
},
{ type: ComponentTypes.FOOTER, data: { info: ["footer"] } }
];
return (
<div>
{data.map((component) => {
return table[component.type](component.data);
})}
</div>
);
}
Проблема в том, что TypeScript ругается на вызов функции из таблицы:
Я знаю как это исправить:
function isHeader(comp: any): comp is IHeaderData {
return comp.type === ComponentTypes.HEADER;
}
export default function App() {
const data: (IFooterData | IHeaderData)[] = [
{
type: ComponentTypes.HEADER,
data: {
login: "Login",
title: "Title"
}
},
{ type: ComponentTypes.FOOTER, data: { info: ["footer"] } }
];
return (
<div>
{data.map((component) => {
if (isHeader(component)) {
return table[component.type](component.data);
} else {
return table[component.type](component.data);
}
})}
</div>
);
}
Пришлось создать кастомную проверку на принадлежность к интерфейсу. Но если следовать этому решению, то когда будет расти число компонентов, то будет так же и расти размер блока if else. Для каждого нового компонента придется создавать новую кастомную проверку ака isFooter, isSomethingElse. Почему изначальный вариант не работает, ведь я обращаюсь к строго заданному полю type интерфейсов IHeaderData или IFooterData, следовательно, в таблице мог бы "забрать" нужную рендер-функцию и передать подходящие ей пропсы? Есть ли какой-то способ обойти бесконечные if else'ы и кастомные проверки? Иными словами, как можно сделать по другому?
Код в codesandbox