Home > Back-end >  How to infer types in a destructured array from arguments?
How to infer types in a destructured array from arguments?

Time:02-21

In the following code, a and b are an OR list of types in the array argument passed into settleAll.

(async () => {
  const settleAll = async <T>(promises: T[]): Promise<Awaited<T>[]> => {
    const results = await Promise.allSettled(promises);
    return results.map(result => (result as PromiseFulfilledResult<any>).value);
  };

  const [a, b] = await settleAll([Promise.resolve(1), Promise.resolve("1")]); // [ a: string | number", b: string | number ]
})();

What I am wanting is to specifically type the output destructured variables as they are input.

For example [ a: number, b: string ] instead of [ a: string | number, b: string | number ].

How would I achieve this?

CodePudding user response:

Using as assertion to make it a tuple (optionally readonly with as const):

(async () => {
  const settleAll = async <T extends readonly Promise<unknown>[]>(promises: T): Promise<{ -readonly [K in keyof T]: Awaited<T[K]> }> => {
    const results = await Promise.allSettled(promises);

    return results.map((result) => (result as PromiseFulfilledResult<any>).value) as { -readonly [K in keyof T]: Awaited<T[K]> };
  };

  const result = await settleAll([Promise.resolve(1), Promise.resolve("1")] as const);
  //    ^?
})();

We take a generic T that is an array of Promises... Next we remove the possible readonly's on the array (or else result will have readonly which might not be desirable), and await the value of the promise in the array with a mapped type.

Finally we use a cast to cast the return value and tell TypeScript to shut up.

Unfortunately there has to be an as assertion as we cannot infer array literal types as tuples right now.

Playground

CodePudding user response:

You should just use Promise.all() in this case.


In your code, you are making an unsafe assertion on each of the mapped PromiseSettledResult values after using Promise.allSettled():

TS Playground

(async () => {
  const settleAll = async <T extends PromiseLike<any>>(promises: T[]): Promise<Awaited<T>[]> => {
    const results = await Promise.allSettled(promises);

    // return results.map(result => result.value);
    //                                     ^^^^^
    // Property 'value' does not exist on type 'PromiseSettledResult<Awaited<T>>'.
    //   Property 'value' does not exist on type 'PromiseRejectedResult'.(2339)

    return results.map(result => (result as PromiseFulfilledResult<any>).value); /*
                                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    As you can see from above, this assertion is unsafe: accessing the `value` property
    on a rejected PromiseSettledResult will evaluate to undefined at runtime */
  };

  const [a, b] = await settleAll([Promise.resolve(1), Promise.resolve("1")]);
})();

You don't account for the possibility of promises rejecting, so those cases are unhandled. This is what Promise.all does (and will reject if any of the promises reject), so you can just use it directly and get the inferred types you want:

TS Playground

const [a, b] = await Promise.all([
  Promise.resolve(1),
  Promise.resolve("1"),
]);

a; // number
b; // string

If you want to handle promise rejections within your function, discarding any settled rejection reasons and simply mapping those values to undefined, then you could do something like this:

TS Playground

type AwaitedValues<T extends readonly unknown[]> = { -readonly [K in keyof T]: Awaited<T[K]> };
type MaybeValues<T extends readonly unknown[]> = { [K in keyof T]: T[K] | undefined };

function settledValueOrUndefined <T, R extends PromiseSettledResult<T>>(result: R): T | undefined {
  return result.status === 'fulfilled' ? result.value : undefined;
}

async function settleAll <T extends readonly PromiseLike<any>[]>(promises: T): Promise<MaybeValues<AwaitedValues<T>>> {
  return (await Promise.allSettled(promises)).map(settledValueOrUndefined) as MaybeValues<AwaitedValues<T>>;
}

const [a, b] = await settleAll([Promise.resolve(1), Promise.resolve("1")] as const); /*
                                                                          ^^^^^^^^
                                Use "as const" assertion to infer from readonly tuple */

a; // number | undefined
b; // string | undefined
  • Related