Typing a React Component Factory Function
Asked Answered
P

2

5

Given the type

type EnumerableComponentFactory = <C, I>(config: {
  Container: React.ComponentType<C>;
  Item: React.ComponentType<I>;
}) => React.FC<{ items: I[] }>;

with the following implementation

const Enumerable: EnumerableComponentFactory =
  ({ Container, Item }) =>
  ({ items }) =>
    (
      <Container>
        {items.map((props, index) => (
          <Item key={index} {...props} />
        ))}
      </Container>
    );

and intended use

const UnorderedList = Enumerable({
  Container: ({ children }) => <ul>{children}</ul>,
  Item: ({ title }: { title: string }) => <li>{title}</li>,
});

<UnorderedList items=[{title: "Something"}] />

I'm observing the following TypeScript error

Type '{ children: Element[]; }' is not assignable to type 'C'.
  'C' could be instantiated with an arbitrary type which could be unrelated to '{ children: Element[]; }'.ts(2322)

which leads me to my question: What type constraints do I need to set up to resolve this error?

I've tried to change the type as follows:

type EnumerableComponentFactory = <C extends { children?: Element[] }, I>(config: {
  Container: ComponentType<C>;
  Item: ComponentType<I>;
}) => (props: { items: I[] }) => ReturnType<FC<I>>;

but this produces an even more cryptic error message, which I'm going to omit for the sake of brevity.


P.S. The function itself actually does exactly what's expected. It's just the compiler that trips up.

Pyro answered 15/7, 2021 at 16:54 Comment(0)
E
1

Is it necessary to keep C generic parameter?

import React, { FC, ComponentType, PropsWithChildren } from "react";

type EnumerableComponentFactory = <I>(config: {
  // or Container: FC<{ children: JSX.Element[] }>;
  Container: FC<PropsWithChildren<object>>;
  Item: ComponentType<I>;
}) => FC<{ items: I[] }>;

const Enumerable: EnumerableComponentFactory =
  ({ Container, Item }) =>
  ({ items }) =>
    (
      <Container>
        {items.map((props, index) => (
          <Item key={index} {...props} />
        ))}
      </Container>
    );

const UnorderedList = Enumerable({
  Container: ({ children }) => <ul>{children}</ul>,
  Item: ({ title }: { title: string }) => <li>{title}</li>,
});

const result = <UnorderedList items={[{ title: "Something" }]} />;

Enchilada answered 18/7, 2021 at 20:57 Comment(0)
G
1

I was able to alter your code to make it work, while also accepting other props to be passed to the container:

type EnumerableComponentFactory = <C, I>(config: {
    Container: React.ComponentType<C & { children: React.ReactNode[] }>;
    Item: React.ComponentType<I>;
}) => React.ComponentType<C & { items: I[] }>;

const Enumerable: EnumerableComponentFactory = ({ Container, Item }) => (
    props
) => (
    <Container {...props}>
        {props.items.map((props, index) => (
            <Item key={index} {...props} />
        ))}
    </Container>
);

Which allows for e.g. this:

const ContainerWithBorder: React.ComponentType<{ color: string }> = (props) => (
    <div style={{ border: `2px solid ${props.color}` }}>
        <ul>{props.children}</ul>
    </div>
);

const ComplexList = Enumerable({
    Container: ContainerWithBorder,
    Item: ({ title }: { title: string }) => <li>{title}</li>
});

<ComplexList items={[{ title: "Something" }]} color="red" />

The ComplexList component comes with typing/intellisense for the color property.

A playground with the original and ComplexList example can be found here.

Grosso answered 23/7, 2021 at 17:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.