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