0
votes

There are a number of places in my application where I am looping through an array in order to build a list of <option> tags. I am trying to create a generic function to extract this functionality.

function optionValues<T, K extends keyof T>(entities: T[], label: K, value: K) {
  return entities.map((e, index) => {
    return <option key={index} value={e[value]}>{ e[label] }</option>
  })
}

const users = [
  { id: 1, name: "Foo"},
  { id: 2, name: "Bar"},
  { id: 3, name: "Baz"}
]

optionValues(users, "name", "id");

// Expected Output
// <option key="0" value="1">Foo</option>
// <option key="1" value="2">Bar</option>
// <option key="2" value="3">Baz</option>

However, TypeScript gives me this slightly hard to understand error at compile time:

TS2322: Type 'T[K]' is not assignable to type 'string | number | readonly string[] | undefined'.   Type 'T[keyof T]' is not assignable to type 'string | number | readonly string[] | undefined'.     Type 'T[string] | T[number] | T[symbol]' is not assignable to type 'string | number | readonly string[] | undefined'.       Type 'T[string]' is not assignable to type 'string | number | readonly string[] | undefined'.         Type 'T[string]' is not assignable to type 'readonly string[]'.           Type 'T[keyof T]' is not assignable to type 'readonly string[]'.             Type 'T[K]' is not assignable to type 'readonly string[]'.               Type 'T[keyof T]' is not assignable to type 'readonly string[]'.                 Type 'T[string] | T[number] | T[symbol]' is not assignable to type 'readonly string[]'.                   Type 'T[string]' is not assignable to type 'readonly string[]'. index.d.ts(2210, 9): The expected type comes from property 'value' which is declared here on type 'DetailedHTMLProps<OptionHTMLAttributes, HTMLOptionElement>'

Which I think means that the value on <option> must be a certain type and my typings can't guarantee it will conform.

How can I change my function so that it will satisfy <option>'s value type?

2
Does this error happen when you call a function? Can you provide more context? - Николай Гольцев
@НиколайГольцев Edited my question to add a bit more context. - jbeast

2 Answers

0
votes

I think it's better to stick with a certain type for option. assume you have the following list:

const users = [
  { id: 1, name: "Foo", extraInfo: { address: "..." } },
  { id: 2, name: "Bar", extraInfo: { address: "..." } },
  { id: 3, name: "Baz", extraInfo: { address: "..." } }
];

optionValues(users, "name", "extraInfo")

in the above example extraInfo is an object but according to @types/react option should follow this type:

interface OptionHTMLAttributes<T> extends HTMLAttributes<T> {
  disabled?: boolean;
  label?: string;
  selected?: boolean;
  value?: string | string[] | number;
}

optionValues guarantee its parameter is ok but option`s value, no.

0
votes

You can resolve the issue by using some helper type which constructs template for option data based on L - label field name and V - value field name:

type OptionTemplate<L extends string, V extends string, LV = string, VV = string | number> = {
  [key in L]: LV;
} & {
  [key in V]: VV;
}

After you need to change the signature of your optionValues function:

function optionValues<L extends string, V extends string, T extends OptionTemplate<L, V>>(entities: T[], label: L, value: V) {
  return entities.map((e, index) => {
    return <option key={index} value={e[value]}>{ e[label] }</option>
  })
}

And finally here is the full code:

type OptionTemplate<L extends string, V extends string, LV = string, VV = string | number> = {
  [key in L]: LV;
} & {
  [key in V]: VV;
}

function optionValues<L extends string, V extends string, T extends OptionTemplate<L, V>>(entities: T[], label: L, value: V) {
  return entities.map((e, index) => {
    return <option key={index} value={e[value]}>{ e[label] }</option>
  })
}

const users = [
  { id: 1, name: "Foo"},
  { id: 2, name: "Bar"},
  { id: 3, name: "Baz"}
]

optionValues(users, "name", "id");