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?
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.