6

I'm trying to create a map of pages for my React Router configuration under TypeScript. Since each page will be generated by different component types, and those different components will have different properties, I'd like to use a generic container, but retain the semantic link between a component and its props interface.

I was hoping to have each element of the map be defined by a few fields, nominally it's name, the component that displays the page, and the relevant props for that component. I thought generics seemed a sensible way to go about this:

type PageDef<T> = {
    name: string;
    component: FunctionComponent<T>;
    props: T;
}

(Note: I also tried this as an interface, but had the same issue.)

For this example, let us assume I've defined two props interfaces, and two corresponding functional components (in reality there are quite a few more...):

export interface PropsOne {
    test_1: string
}

export interface PropsTwo {
    test_2: number
}

function ComponentOne(props: PropsOne) {
    return <div> {props.test_1} </div>
}

function ComponentTwo(props: PropsTwo) {
    return <div> {props.test_2} </div>
}

I then create a page map like so:

type SupportedPages = PageDef<PropsOne> | PageDef<PropsTwo>;

const pages: Array<SupportedPages> = [
    {
        name: "Page one",
        component: ComponentOne,
        props: {
            test_1: "Hello, world"
        }
    },
    {
        name: "Page two",
        component: ComponentTwo,
        props: {
            test_2: 42
        }
    }
]

No errors so far!

The problem comes when I go to use pages in my MainPage component:

function MainPage(props: { pages: Array<SupportedPages> }) {
    return <div>
        {props.pages.map(page => {
            const PageComponent = page.component;
            return <PageComponent {...page.props}/>
        })}
    </div>
}

Here I have an error on the PageComponent tag:

TS2322: Type '{ test_1: string; } | { test_2: string; }' is not assignable to type 'IntrinsicAttributes & PropsOne & { children?: ReactNode; } & PropsTwo'.
Property 'test_2' is missing in type '{ test_1: string; }' but required in type 'PropsTwo'.

I don't really understand what this is telling me - it seems that the props type is being inferred as the intersection of the two props? However I'm pretty sure I want it to be the union.

If the page is of type FunctionalComponent<PropsOne>, then props should be of type PropsOne, and it shouldn't care about the fields in PropsTwo, and vice-versa.

I can work around this in the short term by making the fields optional in the props interfaces, but that doesn't really hold to the spirit of TypeScript. I guess maybe having an array of mixed types also isn't particularly great practice...

I'm new to TS, so I suspect I'm misunderstanding something about how I should be inferring the property types. Happy to take suggestions on the best way to architect this.

n00dle
  • 5,949
  • 2
  • 35
  • 48
  • Can we see what the PageComponent looks like? – Jacob Smit Jan 24 '21 at 23:40
  • 1
    @JacobSmit PageComponent is an instance of either `ComponentOne` or `ComponentTwo`. – lawrence-witt Jan 25 '21 at 00:50
  • 1
    This is (yet) another case of TypeScript's lack of support for what I've been calling "correlated union" record types; see [microsoft/TypeScript#30581](https://github.com/Microsoft/TypeScript/issues/30581). The only reasonable way out of this for now is a type assertion like `as any` or something a little less permissive but still technically unsafe [like this](https://tsplay.dev/GNlkRw). – jcalz Jan 25 '21 at 02:08

0 Answers0