Issue
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
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
Answered By - jcalz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.