I have this issue with tsx. I'm not sure how to compose the type for generics.
In my Options component I have an interface that gets display
(display is property name of T, value will be passed as string) that should be a key in T. T will hold an array of PETS or USERS. The extends {id: string}
here is to ensure that the PETS or USERS has an ID property.
These links are my reference.
https://www.typescriptlang.org/docs/handbook/advanced-types.html#index-types
How can I resolve this problem?
import React from "react";
interface IOptions<T, K extends keyof T> {
display: K;
values: T[];
}
const Options = <Y extends {id: string}, U>({values, display}: IOptions<Y, U>) => {
return (
<>
{values.map((val: Y, index: number) => (
<option key={index} value={val.id}>{val[display]}</option>
))}
</>
);
};
export default Options;
I also tried this:
<option key={index} value={val.id}>{val[display as keyof Y]}</option>
Type 'U' does not satisfy the constraint 'keyof Y'.
...and:
Type 'U' cannot be used to index type 'Y'.
(Y
in your screenshot)
You need to constrain U
so that it is compatible with the constraint in IOptions
as well:
const Options = <Y extends { id: string }, U extends keyof Y>({ values, display }: IOptions<Y, U>) => {}
This directly solves both current errors, because now U
is explicitly a key of Y
.
But then you will have another error appearing:
Type 'Y[U]' is not assignable to type 'ReactNode'.
That is because you constrain Y
for id
only, but it can have anything for its other keys, in particular values which are not suitable for rendering, e.g.:
const y = {
id: "foo",
bar: function () {} // Not suitable as ReactNode
} satisfies { id: string }
Therefore you will have to further constrain your generic type parameter Y
, to ensure that Y[U]
can be rendered, for example if all values are ReactNode
:
const Options = <Y extends {
id: string,
[key: PropertyKey]: React.ReactNode
}, U extends keyof Y>({ values, display }: IOptions<Y, U>) => {}
Or we could be more specific, by constraining only the value of the U
key to be renderable:
const Options = <
U extends PropertyKey,
Y extends {
[key in U]: React.ReactNode // Only value of U key must be a ReactNode
} & { id: string }
>({ values, display }: IOptions<Y, U>) => {}
Then you can pass values which other (non displayed) properties can be anything else:
const other = {
id: "",
foo: 0,
content: <b>Bold</b>,
notRenderable: () => { }
};
<Options
values={[other]}
display="content" // Okay with "id", "foo" or "content"
/>
<Options
values={[other]} // Types of property 'notRenderable' are incompatible. Type '() => void' is not assignable to type 'ReactNode'.
// ~~~~~
display="notRenderable"
/>