reactjstypescripttypescript-genericsreact-typescript

How to include object keys into Generic types


enter image description here

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

https://react-typescript-cheatsheet.netlify.app/docs/advanced/patterns_by_usecase/#generic-components

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>

Solution

  • 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>) => {}
    

    Playground Link


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

    Playground Link