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.
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()
:
(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:
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:
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