'Is it possible to narrow the type of generic parameter T to avoid casting to unknown

I'm trying to add type information to an array sorting function in TypeScript.

Here's the syntax I'm after:

interface Car {
    color: string;
    modelYear: number;
    createdDate: Date;
}

const cars: Car[] = [{
    color: 'red',
    modelYear: 2000,
    createdDate: new Date()
}];

sortArrayBy(cars, ['createdDate'], 1); // invalid syntax, date is not a string or number, this should not compile.
sortArrayBy(cars, ['color', 'modelYear'], 1); // valid syntax, this should compile

Original code

export type KeysOfType<T, KeyType> = { [k in keyof T]: T[k] extends KeyType ? k : never }[keyof T];

type StringOrNumber = string | number;

export function sortArrayBy<T, TY extends KeysOfType<T, StringOrNumber>>(
    arr: T[],
    properties: TY[],
    sortOrder: 1 | -1
): T[] {
    const copy = [...arr];

    const sortByProperty = (property: TY) => (a: T, b: T) => {
        const aVal = a[property] as StringOrNumber; // does not compile
        const aVal = a[property] as unknown as StringOrNumber; // compiles


    }
    return [];
}

The syntax works like I want, but I've had to resort to a cast to as unknown in the function to get the code to compile.

I'm wondering, is there a way to narrow the generic parameter T (or other ideas) so that I can remove the cast?

Without the cast, the full error reads

Conversion of type 'T[TY]' to type 'StringOrNumber' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
  Type 'T[KeysOfType<T, StringOrNumber>]' is not comparable to type 'StringOrNumber'.
    Type 'T[T[keyof T] extends StringOrNumber ? keyof T : never]' is not comparable to type 'StringOrNumber'.
      Type 'T[keyof T]' is not comparable to type 'StringOrNumber'.
        Type 'T[string] | T[number] | T[symbol]' is not comparable to type 'StringOrNumber'.
          Type 'T[symbol]' is not comparable to type 'StringOrNumber'.

The error hints about converting to unknown first, which is why I did that, I'm just curious about other solutions.

I have a TS playground setup with the problem.



Solution 1:[1]

Your problem is that the compiler isn't smart enough to see that T[KeysOfType<T, V>] must be assignable to V for generic T. See microsoft/TypeScript#30728 for more information.

What you can do instead is to constrain the generic object type T to Record<K, string | number> as well as constraining the key type K to KeysOfType<T, string | number>. (I'm calling the key type the more conventional K here instead of TY, if you don't mind.) In some sense this is a circular and redundant constraint, but the compiler is happy with it. And when you have a type T constrained to Record<K, V>, the compiler is smart enough to see that T[K] is assignable to V:

export function sortArrayByExample<
    T extends Record<K, string | number>,
    K extends KeysOfType<T, string | number>
>(arr: T[], keys: K[]) {
    const sortByProperty = (property: K) => (a: T, b: T) => {
        const aVal: StringOrNumber = a[property]; // okay
        const bVal: StringOrNumber = b[property]; // okay    
    };
}

The T extends Record<K, string | number> is for the benefit of the compiler inside the implementation of the function, while K extends KeysOfType<T, string | number> is for the benefit of the caller so that they get both IntelliSense and useful error messages:

sortArrayByExample(cars, ['createdDate']); // error
// ---------------------> ~~~~~~~~~~~~~
// Type '"createdDate"' is not assignable to type 'KeysOfType<Car, string | number>'

sortArrayByExample(cars, ['color', 'modelYear']); // okay

Playground link to code

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 jcalz