My suggestion for createCompoundResult() is this:
function createCompoundResult<T extends any[]>(
queryResults: readonly [...{ [I in keyof T]: QueryResult<T[I]> }],
callback: (queryResultDatas: readonly [...T]) => any
) {
const datas = queryResults.map((queryResult) => queryResult.data) as T;
return callback(datas);
}
The function is generic in T, corresponding to the tuple of arguments to callback. In order to describe the type of queryResults in terms of the array/tuple type T, we want to map it to another array/tuple type where for each numeric index I of T, the element type T[I] gets mapped to QueryResult<T[I]>. So if T is [string, number], then we want queryResults to be of type [QueryResult<string>, QueryResult<number>].
You can do this via a mapped type. It looks like { [I in keyof T]: QueryResult<T[I]> }. For array-like generic types T, mapped types like [I in keyof T] only iterate over the numeric-like keys I (and skip all the other array keys like "push" and "length"). So you can imagine { [I in keyof T]: QueryResult<T[I]> } acting on a T of [string, boolean] operating on I being "0" and then "1", and T["0"] is string and T["1"] is boolean, so you get {0: QueryResult<string>, 1: QueryResults<boolean>}, which is magically interpreted as a new tuple type [QueryResult<string>, QueryResult<boolean>].
That's the main explanation, although there are a few outstanding things to mention.
First is that the compiler does not know that the array map() method will turn a tuple into a tuple, and it definitely doesn't know that queryResult => queryResult.data will turn a tuple of type { [I in keyof T]: QueryResult<T[I]> } into a tuple of type T. (See this question for more info.) It sees the output type of your queryResults.map(...) line as T[number][], meaning: some array of the element types of T. It has lost length and order information. So we have to use a type assertion to tell the compiler that the output of queryResults.map(...) is of type T, so that datas can be passed to callback.
Next, there are a few places where I've wrapped an array type AAA in readonly [...AAA]. This uses variadic tuple type syntax to give the compiler a hint that we'd like it to infer tuple types instead of array types. If you don't use that, then something like [fooQueryResult, barQueryResult] will tend to be inferred as an array type Array<QueryResult<Foo> | QueryResult<Bar>> instead of the desired tuple type [QueryResult<Foo>, QueryResult<Bar>]. Using this syntax frees us from needing to use a tuple() helper function, at least if you pass the array literal directly.
Anyway, let's make sure it works:
class Foo { x = 1 }
class Bar { y = 2 }
createCompoundResult(
[fooQueryResult, barQueryResult],
(datas) => {
const [foo, bar] = datas;
foo.x
bar.y
}
);
Looks good. I gave some structure to Foo and Bar (it's always recommended to do so even for example code) and sure enough, the compiler understands that datas is a tuple whose first element is a Foo and whose second element is a Bar.
Playground link to code