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');
CodePudding user response:
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))
CodePudding user response:
I think I got this, but I'm not sure if there are other/better solutions:
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
) {
return list
.slice()
.sort(
(a, b) =>
(a[propertyKey] as unknown as number) -
(b[propertyKey] as unknown as number)
);
}
This is me saying to typescript: "It looks like you still don't understand that type of a[propertyKey] is a number, but I know it's a number so I'm casting it as number".
