Building a better Promise.all, or, is it possible to infer and unbox generics in a TS object passed to a function at compile time?

Advertisements

I’m working on building a better implementation of Promise.all which takes an object of string keys and Promises. The goal is that the result of a successful run will map the Promise results to their corresponding keys while preserving the underlying type information:

// compiles and properly infers result type
const result: number = await resolveAll({
  aNumber: Promise.resolve(1),
  anObject: Promise.resolve({foo: 1, bar: "baz"}),
}).then(({ 
  aNumber,            // typeof number 
  anObject: { foo }   // typeof {foo: number, bar: string}
}) => aNumber + foo)  

My question is whether or not it is possible to define resolveAll and the corresponding object it returns (ostensibly one which exposes a then method) such that they preserve the type information of the generic types encapsulated by the Promise values passed in as an argument.

It seems as though a solution to this problem would necessitate Rank-2 Types (What is the purpose of Rank2Types?) but I’m sufficiently out of my depth in Typescript that I’m not sure whether or not this is possible using an existing feature of the type system.

Thanks in advance for the help!

>Solution :

There is no need for any complicated magic, just a generic parameter and the built-in Awaited utility.

function resolveAll<T extends Record<any, Promise<any>>>(
  t: T
): Promise<{
  [K in keyof T]: Awaited<T[K]>;
}> {
  const entries = Object.entries(t);
  return Promise.all(entries.map(([_, v]) => v)).then((res) =>
    entries.reduce(
      (acc, [k, _], i) => ({
        ...acc,
        [k]: res[i],
      }),
      {} as { [K in keyof T]: Awaited<T[K]> }
    )
  );
}

Playground

Leave a ReplyCancel reply