Search code examples
typescript

Error getting the type of key whilst mapping a collection


Some context: I am mapping a collection of objects, with the intention to pick the values of one member from that object.

type RadioOption = {
  value: string
  label: string
}

const requestTypeOptions = [
  { value: "question", label: "I have a question" },
  { value: "problem", label: "I have a problem" },
  { value: "complaint", label: "I have a complaint" },
] as const satisfies RadioOption[];

function pickKeyValue<C extends {[k: string]: any}[], K extends keyof C[number]>(collection: C, pick: K) {
  return collection.map((obj, i) => {
    return obj[pick]
    //         ^^^^    <- error
  }) as { [I in keyof C]: C[I][K] };
}

const pickedValues = pickKeyValue(requestTypeOptions, 'value')
// pickedValues: ["question", "problem", "complaint"]

The compiler returns the expected type for pickedValues, but there still is an error in the function itself.

It turns out that pick is of type string | number | symbol, which can not be used to index {[k: string]: any}.

edit: I understand now that {[k: string]: any} does not restrict the key to only be string (thanks jcalz).

But how do I make it so that pick is only of type string, so that it works as intended?

Playground


Solution

  • The problem isn't that pick isn't known to be string, but that it isn't known to be keyof obj. And that's because obj is widened from the generic C[number] to its constraint, {[k: string]: any}. This widening tends to happen automatically as a simplification (see microsoft/TypeScript#33181 for a similar case). So the smallest change we can make is to annotate obj as C[number]:

    function pickKeyValue<
      C extends { [k: string]: any }[],
      K extends keyof C[number]
    >(collection: C, pick: K) {
      return collection.map((obj: C[number], i) => {
        return obj[pick]
      }) as { [I in keyof C]: C[I][K] };
    }
    

    Or you could refactor your constraints to something TypeScript will handle more naturally, such as allowing K to be anything keylike, and then constraining C appropriately:

    function pickKeyValue<
      K extends PropertyKey,
      C extends ({ [k: string]: any } & Record<K, any>)[]
    >(collection: C, pick: K) {
      return collection.map(obj => {
        return obj[pick]
      }) as { [I in keyof C]: C[I][K] };
    }
    

    That works because now obj isn't widened, and its type is effectively Record<K, any>, and K is known to be keyof that.


    Note that in neither case do I care if K is a string or some other keylike thing such as a number or symbol. If you really care about that you can add string to the constraint for K (e.g., & string), but this does seem to be beside the point. You only cared about string because obj was being widened to something with a string index signature. But that was the problem itself.

    Playground link to code