Typing Promise.all over a heterogeneous tuple of promises
loadAll must resolve to the tuple [User, number, boolean] — inferred, not annotated and not asserted. Right now the promises are collected into an array variable before being awaited, and that variable widens them into a single union-typed array.
Constraints: no as assertions, no any, no explicit return-type annotation on loadAll. The tuple must come out of inference alone.
interface User { id: string }
declare const userP: Promise<User>;
declare const countP: Promise<number>;
declare const flagP: Promise<boolean>;
async function loadAll() {
const pending = [userP, countP, flagP];
// your code here
}
Write the implementation.
Promise.all has a tuple overload that maps each position through Awaited<T[P]>, so every slot keeps its own type. It only fires while the argument is still a tuple: stored in a plain const array the elements widen to (Promise<User> | Promise<number> | Promise<boolean>)[], collapsing the result to a union array. Keep the tuple with as const, or pass the array literal inline.
- ✗Assuming a
constarray of promises stays a tuple instead of widening to an array - ✗Reaching for
as [User, number, boolean]instead of preserving tuple inference - ✗Thinking
Promise.allcan only ever produce an array of a union
- →How does
Promise.allSettledchange the element type of the resulting tuple? - →Why does passing the array literal directly to
Promise.allalready infer a tuple?
Why inference breaks
const pending = [userP, countP, flagP] is a mutable array, not a tuple. TypeScript widens the literal to (Promise<User> | Promise<number> | Promise<boolean>)[]: the length is forgotten and so are the positions. Promise.all's tuple overload does not fire on that argument, and the result degrades to (User | number | boolean)[].
The fix
Pin the tuple with as const — that is the only missing piece:
interface User { id: string }
declare const userP: Promise<User>;
declare const countP: Promise<number>;
declare const flagP: Promise<boolean>;
async function loadAll() {
const pending = [userP, countP, flagP] as const;
// ^? readonly [Promise<User>, Promise<number>, Promise<boolean>]
return Promise.all(pending);
// ^? Promise<[User, number, boolean]>
}
const [user, count, flag] = await loadAll();
// ^User ^number ^boolean
The signature in lib.es2015.promise.d.ts is a mapped type over the tuple's positions:
all<T extends readonly unknown[] | []>(
values: T,
): Promise<{ -readonly [P in keyof T]: Awaited<T[P]> }>;
{ -readonly [P in keyof T]: Awaited<T[P]> } walks the tuple's indices, strips readonly, and unwraps each position through Awaited. That is exactly why the readonly tuple produced by as const fits and a widened array does not.
The equivalent alternative is to skip the variable entirely: an array literal passed straight into the call is contextually typed by the parameter T and infers as a tuple too.
async function loadAll() {
return Promise.all([userP, countP, flagP]); // also Promise<[User, number, boolean]>
}