1

Given:

class Foo {}
class Bar {}

interface QueryResult<T> {
  data: T;
}

function creatQueryResult<T>(data: T): QueryResult<T> {
  return {data};
}

function tuple<T extends any[]>(...args: T): T {
  return args;
}

I want to create and type a function using inferred types that accepts QueryResult[] or a tuple of QueryResults and a factory callback that picks data and invokes the callback like:

function createCompoundResult<T>(
  queryResults: T,
  callback: (queryResultDatas: ???<T>) => any
) {
  const datas = queryResults.map((queryResult) => queryResult.data);

  return callback(datas);
}

Note the ??? in the above code.

Usage:

const fooQueryResult = creatQueryResult(new Foo());
const barQueryResult = creatQueryResult(new Bar());

// Maybe using tuples are wrong?
const queryResults = tuple(fooQueryResult, barQueryResult);

createCompoundResult(
  queryResults,
  (datas) => {
    const [foo, bar] = datas;
    // foo should be inferred as Foo here and bar as Bar
  }
);

Maybe tuples are the wrong way to go? How would you solve it?

I'm a bit of a TypeScript newbie and I have a really hard time understanding stuff like keyof, extends keyof, { [K in keyof T]: { a: T[K] } } so if your solutions include arcane magic like this, please explain it to me like I'm 5 years old.

2
  • 1
    Does this approach work for your needs? If so I can write up an answer explaining it (although any explanation I give would be unlikely to help a kindergartner). If not, let me know what I'm missing Commented Feb 25, 2022 at 19:01
  • @jcalz That works like a charm. Thank you very much. Be sure to add it as an answer so I can mark and point it. Commented Feb 25, 2022 at 19:14

1 Answer 1

2

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

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.