Is there a way to make the following type check?
function getNumberFromObject<T>(obj: T, key: keyof T): number {
return obj[key] // ERROR: obj[key] might not be a number
}
I want to specify that key
should not only be a key of T
, but a key with a number
value.
The most straightforward way to do this so that both the callers and the implementation of getNumberFromObject<T>
type check correctly is this:
function getNumberFromObject<T extends Record<K, number>, K extends keyof any>(
obj: T,
key: K
): number {
return obj[key] // okay
}
And when you call it:
getNumberFromObject({dog: 2, cat: "hey", moose: 24}, "dog"); // okay
getNumberFromObject({dog: 2, cat: "hey", moose: 24}, "cat"); // error
getNumberFromObject({dog: 2, cat: "hey", moose: 24}, "moose"); // okay
getNumberFromObject({dog: 2, cat: "hey", moose: 24}, "squirrel"); // error
That all works well, except that the errors you get are a little obscure in that it complains that Object literal may only specify known properties, and 'dog' does not exist in type 'Record<"somebadkey", number>'
. This complaint is an excess property check and isn't really the issue.
If you want to make it so that callers get a better error, you could use a more complicated conditional type like this:
function getNumberFromObject<T, K extends keyof any & {
[K in keyof T]: T[K] extends number ? K : never
}[keyof T]>(
obj: T,
key: K
): T[K] {
return obj[key] // okay
}
getNumberFromObject({ dog: 2, cat: "hey", moose: 24 }, "dog"); // okay
getNumberFromObject({ dog: 2, cat: "hey", moose: 24 }, "cat"); // error
getNumberFromObject({ dog: 2, cat: "hey", moose: 24 }, "moose"); // okay
getNumberFromObject({ dog: 2, cat: "hey", moose: 24 }, "squirrel"); // error
In this case, T
is unconstrained, but K
is forced to be just those keys from T
where T[K]
is a number
.
Now the error says Argument of type '"somebadkey"' is not assignable to parameter of type '"dog" | "moose"'.
, which is more developer-friendly. Not sure if the extra complexity of signature is worth it to you, though.
Hope that helps. Good luck!
Update: The latter function returns T[K]
, not number
. That could be a good thing, since T[K]
is possibly more specific than number
. For example:
interface Car {
make: string,
model: string,
horsepower: number,
wheels: 4
}
declare const car: Car;
const four = getNumberFromObject(car, 'wheels'); // 4, not number
The value four
is of type 4
, which is more specific than number
. If you really want to widen the return type of the function to number
, you can... although the implementation will balk at that since the compiler isn't smart enough to realize that T[K]
is assignable to number
in the generic case. There are ways to deal with that, but the easiest is to use a type assertion in the implementation (return obj[key] as any as number
).