David Barker
David Barker

Reputation: 14620

Use a generic types value for a property expecting 'number | string | undefined'

This question uses React code but is specific to typescript rather than react.

I am using a simplified example to try and make this easier to read. I have a component MyList that takes one generic type argument that is passed to the props type.

This generic type infers an object to be used to generate a child component. I want to key each child component using a value of T[keyof T] || its index in T[].

Creating the list and using the index is no problem but I can't figure out how to correctly type listKey so that it will work with reacts key property that is expecting number | string | undefined.

type MyListProps<T> = {
  list: T[],
  listKey: keyof T,
  contentKey: keyof T
}

class MyList<T extends object> extends React.Component<MyListProps<T>, any> {
  public render() {
    return (
      <div>
        { this.props.list.map((value: T, index: number) => {
            const key = value[this.props.listKey];
            return (
              // Type T[keyof T] is not assignable 
              // to type 'number | string | undefined'
              <Item key={key}>{value[this.props.contentKey]}</Item>
            );
        }
      </div>
    );
  }
}

How could I infer that the listKey type is assignable to the expected type of reacts key prop using a generic?

Upvotes: 1

Views: 584

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 250006

Since you want to assign the result of the index access to key you will need to restrict what that index operation can return. One way to do that is to use a constraint on T:

class MyList2<T extends Record<string, string | number >> extends React.Component<MyListProps2<T>, any> {
  public render() {
    return (
      <div>
        { this.props.list.map((value: T, index: number) => {
            const key = value[this.props.listKey];
            return (
              <Item key={key}>{value[this.props.contentKey]}</Item>
            );
        })}
      </div>
    );
  }
}

This does have the drawback of restricting the possible values you can have in T which might be a problem (or not depending on your use case).

Another more complicated option is to restrict just what indexing using listKey will return. This does require an extra type parameter, but it allows more flexibility:

type MyListProps<T, LK extends PropertyKey> = {
  list: Array<T & Record<LK, string>>,
  listKey: LK,
  contentKey: keyof T
}
declare const Item: React.FC<{}>

class MyList<T, LK extends PropertyKey> extends React.Component<MyListProps<T, LK>, any> {
  public render() {
    return (
      <div>
        {this.props.list.map((value, index: number) => {
          const key = value[this.props.listKey];
          return (
            <Item key={key}>{value[this.props.contentKey]}</Item>
          );
        })}
      </div>
    );
  }
}

let x = <MyList listKey="a" contentKey="a" list={[
  { a: "", b: 0, p: {} }
]} ></MyList>

Or you could forgo type safety in this case and just use a type assertion.

Upvotes: 1

Related Questions