Search code examples
reactjstypescriptreact-typescript

How to type prop for keyof nested object in React?


The problem I've encountered is trying to enforce the correct typing on the props for a react component when requiring a nested objects keys.

The object I've been trying to pull data from follows this shape:

interface Test {
    group1: {
        a: string
        b: boolean
        c: number
    }
    group2: {
        g: string
        h: number
        i: boolean
    }
}

The component is called like this:

<MyComponent group="group1" key="a" />

I've tried typing the props of the component like this

interface MyComponentProps {
  group: keyof Test
  key: keyof Test[MyComponentProps['group']]

but vscode intellisense tells me that the key is never instead of the keyof the group object. I've also tried a few generic type definitions but I haven't had any success with them. I assume the problem is because it's trying to reference the MyComponentProps group type which is returning a list of values instead of it's actual value. I'm not entirely sure how to go about fixing this.

I want the key prop to detect the correct possible values from the Test object based on the group prop passed to the component.

I found this post How to type nested properties with keyof? that seems to be trying to do basically the same thing but there's no answer.


Solution

  • First things first: as @Konrad noted, key is a restricted prop in React, because it is used to identify list items. Let's just rename it to which to keep things easy :-)

    Now, let's take a look at MyComponentProps:

    interface MyComponentProps {
      group: keyof Test;
      which: keyof Test[MyComponentProps['group']];
    }
    

    What is MyComponentProps['group']? It's just keyof Test. So, the type is actually this symmetric-looking thing:

    interface MyComponentProps {
      group: keyof Test;
      which: keyof Test[keyof Test];
    }
    

    Why is which inferred as never? Let's look at Test[keyof Test]:

    {
        a: string;
        b: boolean;
        c: number;
    } | {
        g: string;
        h: number;
        i: boolean;
    }
    

    Consider that keyof (X | Y) is not (keyof X) | (keyof Y), it's (keyof X) & (keyof Y) – a value is only a key of the union if it's a key of each member of the union. Since there are no keys in common between Test['group1'] and Test['group2'], keyof Test[keyof Test] is never.

    So, that's why it doesn't work as is. To fix it, you need to provide a generic parameter per @vr.'s comment:

    interface MyComponentProps<Group extends keyof Test> {
      group: Group;
      which: keyof Test[Group];
    }
    
    function MyComponent<Group extends keyof Test>(props: MyComponentProps<Group>)
    {
      /* ... */
    }
    

    Intellisense is happy, Typescript is happy, React is happy.

    <MyComponent group="group1" which="a" />
    <MyComponent group="group2" which="g" />
    <MyComponent group="group0" which="a" />   /* error: group0 is not in keyof Test */
    <MyComponent group="group1" which="g" />   /* error: g is not in "a" | "b" | "c" */