I often have to sort objects by properties, so I wrote the following simple helper:
static sort = <T extends {}>(list: Array<T>, field: keyof T, descending: boolean = true) =>
list.sort((a, b) => {
if (a[field] > b[field]) {
return descending ? -1 : 1;
}
if (a[field] < b[field]) {
return descending ? -1 : 1;
}
return 0;
});
I often have to sort objects by a date-like property on the object, but the dates I receive are stringified in the format "01-Apr-2018"
, so I have to convert them into a Date first.
So, I expanded the above with:
static byDate = <T extends {}>(list: Array<T>, field: keyof T, descending: boolean = true) =>
list.sort((a, b) => {
if (new Date(a[field]).getTime() > new Date(b[field]).getTime()) {
return descending ? -1 : 1;
}
if (a[field] < b[field]) {
return descending ? 1 : -1;
}
return 0;
});
which results in
TS2345: Argument of type 'T[keyof T]' is not assignable to parameter of type 'Date'
on a[field]
and b[field]
.
Tracing WebStorm's / TypeScript Service's implementation of Date that was chosen, I noticed that in new Date("hello")
, it chooses the following interface from lib.es5.d.ts
:
interface DateConstructor {
new(): Date;
new(value: number): Date;
new(value: string): Date; // THIS ONE (which I expect)
new(year: number, month: number, date?: number, hours?: number, minutes?: number, seconds?: number, ms?: number): Date;
(): string;
readonly prototype: Date;
parse(s: string): number;
UTC(year: number, month: number, date?: number, hours?: number, minutes?: number, seconds?: number, ms?: number): number;
now(): number;
}
But in my sort comparator, it chooses the interface from lib.es2015.core.d.ts
, which only has:
interface DateConstructor {
new (value: Date): Date;
}
Attempting to assert with new Date(a[field] as string)
or as Date
has no effect.
I'm using TypeScript 2.7.2 (required by my current Angular 6.0.9 project and can't change).
So:
Why does tsc
choose one over the other in these contexts?
How can I assert which one I want it to use, or at the very least, get my code to compile while maintaining the field: keyof T
parameter?
I can't test on 2.7.2 right now, but the main issue is that TypeScript doesn't believe that a[field]
is necessarily a string
. And that makes sense because a
is of generic type T
, and field
is of type keyof T
, so all it knows is that a[field]
is T[keyof T]
. That might be string
, or it might be something that you can't call new Date()
on.
If you know for sure that you will only call byDate
on something where a[field]
is a string
, then you can change the generics to be like this:
static byDate = <K extends keyof any, T extends Record<K, string>>(
list: Array<T>,
field: K,
descending: boolean = true
) => { ... }
That should make the error go away... it now knows that a[field]
is type T[K]
, where T
extends Record<K, string>
, also known as {[P in K]: string}
, or "an object whose keys are in K
and whose values are string
". So T[K]
extends string
and it's happy.
The above should work on TypeScript 2.7.2 but let me know if you run into a problem.
Hope that helps. Good luck!