UPD: this answer is partly outdated, since late TS 4.x versions introduced tracking relations between destructured variables. However, relations between function arguments will probably not ever be preserved (except maybe for overloaded function, but they are not useful in OP's case), so the suggested solution is still valid.
SOMEWHAT OUTDATED PART
The problem is typescript only keeps track of connections between values' types as long as they are in the same variable, like two properties of one object or two elements of one tuple. As soon as you separate these values into different variables, for example by destructuring them or just passing as 2 distinct arguments in a function, like in your case, all connections are lost and there is no way to restore them (at least for now, maybe one day it will be possible). Consider a situation that may come up very often
type Foo =
| { type: 'a', value: number }
| { type: 'b', value: string }
declare const obj: Foo
// Here there is connection between obj.type and obj.value
switch(obj.type) {
case 'a':
// Here obj.value is of type number
break
case 'b':
// Here obj.value is of type string
break
}
But if you do this
const {type, value} = obj
// At this point type and value are distinct variables
// and any connection they had is lost
switch(type) {
case 'a':
// Here value is number | string
break
case 'b':
// And here as well
break
}
And you cannot do anything about it, you have to keep the values in the same object for typescript to retain any useful information on how their types depend on each other.
VALID SOLUTION
So this is exactly what you could do in your scenario: put these values in an object and don't destructure it. Here is one way. The first attempt could look like this:
declare function orderBy<T, U extends keyof T>(
array: T[],
keys: U[],
sort: (data: {key: U, a: T[U], b: T[U]}) => number
): T[]
But it will actually not work, because T[U]
is number | Date
in the particular example you provided, so the dependency between types is lost right away. To keep it you need to kind of "map" this union type U
so that each member of this union is mapped to an object with fitting types. This can be accomplished with a helper type like this:
type MapKeysToSortData<T, U extends keyof T> = {
[TKey in U]: {
key: TKey,
a: T[TKey],
b: T[TKey],
}
}[U]
So you are creating an object where keys are members of U
, you assign an object to every key like this which is strictly typed, then you get a union of these objects with this [U]
at the end. So the function will look like this:
declare function orderBy<T, U extends keyof T>(
array: T[],
keys: U[],
sort: (data: MapKeysToSortData<T, U>) => number
): T[]
And that's pretty much it. You may have some problems when adding implementation to this declaration, I'm not going to type it here, anyway you can throw in some type assertion and it will do it. Although I'm pretty sure you can type it without type assertions in 2021.
Now use the function without destructuring the argument in the callback (at least up to the moment when typescript already inferred everything you needed)
orderBy([testObject], ['age', 'eta'], (data) => {
if(data.key === 'eta') {
// Here TS already knows data.a and data.b are of type Date, so you can destructure them
const {a, b} = data
console.log(a.toUTCString(), b.toUTCString())
} else {
// Here data.a and data.b are of type number
}
return 1
})
Check out a sandbox
Also I shall note that you are not sorting multidimensional arrays, just one-dimensional arrays of objects. And please don't insert screenshots of your code, copy the actual code as text, and add comments describing these hover thingies of your IDE, it's a bit annoying to copy your test data from an image.