Фабрика компонентов 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


Ответы (0 шт):