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.
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" */