1

I have this simple function for sorting objects by date. But currently I have to check if the field actually is a date, before doing the comparison. Is there a way to limit K to only allow keys that are of a certain type, in this case Date?

const compareDate = <T, K extends keyof T>(key: K) => (x: T, y: T) => {
  const v = x[key];
  const w = y[key];
  return v instanceof Date && w instanceof Date ? v.getTime() - w.getTime() : 0;
};

list.sort(compareDate('dateField'));

What I would want is:

const compareDate = <T, K extends ???>(key: K) => (x: T, y: T) => {
  // ts should know and only allow x[key] and y[key] to be of type Date here:
  return x[key].getTime() - y[key].getTime();
}

const list = [{a: 1, b: 'foo', c: new Date}];

list.sort(compareDate('a')); // <-- ts should refuse this
list.sort(compareDate('b')); // <-- ts should refuse this
list.sort(compareDate('c')); // <-- ts should allow this

Is there a way to express this in Typescript

Svish
  • 152,914
  • 173
  • 462
  • 620

2 Answers2

8

You can use a mapped type to get all Date props of a type:

type DateProps<T> = ({ [P in keyof T]: T[P] extends Date ? P : never })[keyof T];

And then use that instead of keyof T:

const compareDate = <T extends Record<K, Date>, K extends DateProps<T>>(key: K) => (x: T, y: T) => {
    return x[key].getTime() - x[key].getTime();
};

Borrowing the Record idea from @ford04 we can even ensure that TypeScript is aware of the type of x[key] and y[key]. Meaning there is no instanceof check or casting necessary inside the function.

Playground

lukasgeiter
  • 147,337
  • 26
  • 332
  • 270
  • 1
    That does limit the key name as I want, but Typescript still doesn't know that `x[key]` is of type `Date`? Is there a way to tell Typescript that too? – Svish Sep 03 '19 at 11:40
  • You're right I just noticed that as well. I'm not sure that's possible, you might have to cast them. Will update the answer if I find something though. – lukasgeiter Sep 03 '19 at 11:42
  • Yeah I'm sorry. I don't think there is a way to get it to work without the `instanceof` check or casting (as seen in my updated answer) – lukasgeiter Sep 03 '19 at 11:51
1

You can do it with T extending Record<K, Date> type, where K is some string key of this record. When you invoke compareDate with a key K, that does not point to a Date value inside the record, you get the desired type error.

const compareDate = <T extends Record<K, Date>, K extends string>(key: K) => (
  x: T,
  y: T
) => x[key].getTime() - y[key].getTime();

const list = [{ a: 1, b: "foo", c: new Date() }];

list.sort(compareDate("a")); // <-- error
list.sort(compareDate("b")); // <-- error
list.sort(compareDate("c")); // <-- works!

Playground

ford04
  • 66,267
  • 20
  • 199
  • 171
  • When I changed the generics to this from what I had via @lukasgeiter, it no longer worked, so seems the `Record` type is more limiting in some way? Also, you still have the `instanceof` check. – Svish Sep 04 '19 at 14:58
  • @Svish sorry, had forgotten to drop the instanceof operator, there is no cast/instanceof necessary. The Record type just ensures, that the key you pass in leads to Date values in `x` and `y`. When you define the key as in the other answer, the problem is that the compiler cannot make a connection between `T` and `K`. In this case you would have to cast. Answer is now updated. – ford04 Sep 04 '19 at 16:31
  • In the answer from @lukasgeiter, TypeScript compiler cannot statically analyze and infer the type `T` for `x` and `y` in the function body, because `T` is not restricted. `T` and the conditional type `DateProps` cannot be evaluated until a concrete list and item types are given for which you invoke `list.sort`. – ford04 Sep 04 '19 at 17:14
  • @ford04 I have to agree that using `Record` for this is a good idea. Combining it with my existing answer results in the best of both worlds IMO. The advantage of restricting the type of `key` directly is that the error message is much clearer and even autocompletion works. @Svish see my updated answer now with `Record` and without casts. – lukasgeiter Sep 04 '19 at 17:48