Promise.allSettled but used as Promise.all

109 Views Asked by At

I want to create some generic handling for Promise.allSettled

Currently allSettled returns list of fulfilled and rejected results. I don't care and I really don't want to make handling everywhere of it. I want to use it same as Promise.all if any error, reject, BUT wait till all ends. It's important.

private async allAndWait<T>(values: Iterable<PromiseLike<T>>): Promise<T[]> {
    const allSettled = await Promise.allSettled(values);
    const rejected = allSettled.find((promise) => promise.status === 'rejected') as PromiseRejectedResult | undefined;
    if (rejected) {
        throw rejected.reason;
    }
    return allSettled.map((promise) => (promise as PromiseFulfilledResult<T>).value);
}

Created something like this but it does not allow me to send array of different type of promises. And return type is only of the first type, while I want to have exactly the same as with Promise.all experience

enter image description here

1

There are 1 best solutions below

0
MaximilianMairinger On

TLDR:

async function allAndWait<T extends readonly unknown[] | []>(values: T & Iterable<PromiseLike<unknown>>): Promise<{-readonly [key in keyof T]: Awaited<T[key]>}>
async function allAndWait<T extends PromiseLike<unknown>>(values: Iterable<T>): Promise<Awaited<T>[]>
async function allAndWait<T extends readonly unknown[] | []>(values: Iterable<PromiseLike<unknown>>): Promise<any> {
  const allSettled = await Promise.allSettled(values) as PromiseSettledResult<Awaited<T[number]>>[]
  const rejected = allSettled.find((promise) => promise.status === 'rejected') as PromiseRejectedResult | undefined;
  if (rejected) {
      throw rejected.reason;
  }
  return allSettled.map((promise) => (promise as PromiseFulfilledResult<Awaited<T[number]>>).value)
}

Here is my hopefully comprehensible process:

I don't know why but in your code T doesn't get inferred correctly. I'd have expected that the T to be a union of all types that the input tuple has (so string | number). We can do this by shifting the type deceleration to the generic. (just shifting PromiseLike<unknown> would be enough).

async function allAndWait<T extends Iterable<PromiseLike<unknown>>>(values: T) ...

This admittedly doesn't help us much, as we need T to be a tuple and not a union. As you want the return types to be ordered (and not all of them being typeof the union of all input types). Whether we get such a tuple in the form of [PromiseLike<string>, PromiseLike<number>] or [string, number] doesn't matter much, as we can pluck the types from the former with a mapped type. So assuming we have such a (PromiseLike) tuple T we can simple do:

{[key in keyof T]: Awaited<T[key]>}

My first intuition was to use a rest operator:

async function allAndWait<T extends Promise<unknown>[]>(...values: T): Promise<{[key in keyof T]: Awaited<T[key]>}> {...}

allAndWait(Promise.resolve("qwe"), Promise.resolve(2)) // typeof Promise<[string, number]>

Note that we had to drop the Iteratable over an array. As we cannot use the rest operator on Iteratables. Note that Iteratables do not tuple like types. So we cannot say that the first element is typeof string and the second one typeof number, which we need here. We can later do a overload for this use case (in case you really need it) and provide a less powerful type implementation that works with unions. But first we need to fix that you cant even pass in arrays. You could do it like this:

const arr = [Promise.resolve("string"), Promise.resolve(2)]
allAndWait(...arr)

But this doesn't strictly follow your requested type implementation. Note that we cannot just omit the rest operator from the function decleration, as this would again convert the tuple to a union type. So we have to apply a trick:

async function allAndWait<T extends readonly unknown[] | []>(values: T & Iterable<Promise<unknown>>): Promise<{-readonly [key in keyof T]: Awaited<T[key]>}> {...}

We require two things here via a intersection type: first, values must be typeof Iterable<Promise>. And second, we infer a very generic tuple T, so that we can use it later for the export. This line is heavily inspired by the Promise.all type declaration and I don't know why here: T extends readonly unknown[] | [] the union with [] is necessary, but it is.

This only leaves the less powerful overload for Iteratables:

async function allAndWait<T extends PromiseLike<unknown>>(values: Iterable<T>): Promise<Awaited<T>[]>

Which we have to put below the other overload, as it is more generic and would conform to all possible inputs (so the other overload would never be reached).