Typescript - restrict object key type in generics
I'm trying to create a function that sorts an array of objects by an object key, but typescript is throwing an error saying the object type is not "any", "number", "bigint" nor an "enum". I tried to constrict the type of the key by number
only. It kinda works when I call the function, aka when I try to call the function with a property that is not a number in the argument it shows
Argument of type '"b"' is not assignable to parameter of type 'KeysOfType<{ a: number; b: string; c: number; }, number>'.ts(2345)
but I don't get why in the function itself typescript doesn't recognize that the property is a number, thus throwing me an error. How can I mitigate/solve this issue? Am I approaching the typing incorrectly?
Below what I have with the errors:
type KeysOfType<T, KT> = {
[K in keyof T]: T[K] extends KT ? K : never;
}[keyof T];
export function sortArrayByKey<T, K extends KeysOfType<T, number>>(
list: T[],
propertyKey: K
) {
// The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.ts(2362)
// The right-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.ts(2363)
return list.slice().sort((a, b) => (a[propertyKey] - b[propertyKey]));
}
// works
sortArrayByKey([{a: 1, b: 'asd', c: 3.0}, {a: 2, b: '123', c: 2.1}], 'a');
// Argument of type '"b"' is not assignable to parameter of type 'KeysOfType<{ a: number; b: string; c: number; }, number>'.ts(2345)
sortArrayByKey([{a: 1, b: 'asd', c: 3.0}, {a: 2, b: '123', c: 2.1}], 'b');
Solution 1:
Here you can find similar question, except this question/answer is about using function
constraint instead of number
.
TypeScript is able to validate passed propertyKey
during function call stage, but is unable to validate b[propertyKey]
whether it is a number or not inside function body.
a[propertyKey]
is infered as T[K]
, this type knows nothing whether it is a number or not. It is a black box, because you don't even have a constraint for T
. TS knows only that K
is allowed type for indexing T
. Thats all.
In order to make it work, you should apply a constraint to T
. For example: T extends Record<PropertyKey, number>
. I know, you will say that T
might be any object and it is not necessary that all values are numbers. This is why you need to provide a transition function from any object T
to T extends Record<string, number>
.
Transition function:
const toNumber = <
Obj extends Record<string, any>,
Key extends KeysOfType<Obj, number>
>(obj: Obj, key: Key): number => obj[key]
Please notice, that I have used explicit return type number
. Without explicit number
, return type of this function is T[K]
. The good news, typescript allows us to use explicit number
and do some checking if number
is assignable or not. T
Now, we can write our callback for Array.prototype.sort
and our main function:
const callback =
<Obj,>(propertyKey: KeysOfType<Obj, number>) =>
(a: Obj, b: Obj) =>
toNumber(a, propertyKey) - toNumber(b, propertyKey);
const sortArrayByKey = <T, K extends KeysOfType<T, number>>(
list: T[],
propertyKey: K
) => [...list].sort(callback(propertyKey))
Playground