The short answer
Type the functions that make the promises, and pass the promises to Promise.all as an array literal. The awaited result is a tuple with each type in its place:
/** @typedef {{ id: string, name: string }} Customer */
/** @typedef {{ id: string, total: number }} Order */
/** @param {string} id @returns {Promise<Customer>} */
async function loadCustomer(id) {
return { id, name: "Ada" };
}
/** @param {string} customerId @returns {Promise<Order[]>} */
async function loadOrders(customerId) {
return [{ id: `${customerId}-1`, total: 40 }];
}
/** @param {string} id */
export async function loadDashboard(id) {
const [customer, orders] = await Promise.all([loadCustomer(id), loadOrders(id)]);
return { name: customer.name, spent: orders.reduce((sum, order) => sum + order.total, 0) };
}type Customer = { id: string; name: string };
type Order = { id: string; total: number };
async function loadCustomer(id: string): Promise<Customer> {
return { id, name: "Ada" };
}
async function loadOrders(customerId: string): Promise<Order[]> {
return [{ id: `${customerId}-1`, total: 40 }];
}
export async function loadDashboard(id: string) {
const [customer, orders] = await Promise.all([loadCustomer(id), loadOrders(id)]);
return { name: customer.name, spent: orders.reduce((sum, order) => sum + order.total, 0) };
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": ["node"]
},
"include": ["*.js"]
} customer is a Customer and orders an Order[], so customer.name and order.total are checked with no annotation on either.
Why it works
Promise.all’s type in TypeScript’s library is generic over the array it receives. Given an array literal, tsc infers a tuple, [Promise<Customer>, Promise<Order[]>], and maps it to [Customer, Order[]] with Awaited. Destructuring then takes each element’s own type.
The same thing in TypeScript
The TypeScript equivalent tab is the same code with the parameter and return types written inline. Nothing about Promise.all changes; the inference is identical.
What goes wrong
Build the array first and the tuple is lost. A variable initialized with an array literal is an array, here (Promise<Customer> | Promise<Order[]>)[], so every element is the union of both, and both property reads fail with TS2339:
/** @typedef {{ id: string, name: string }} Customer */
/** @typedef {{ id: string, total: number }} Order */
/** @param {string} id @returns {Promise<Customer>} */
async function loadCustomer(id) {
return { id, name: "Ada" };
}
/** @param {string} customerId @returns {Promise<Order[]>} */
async function loadOrders(customerId) {
return [{ id: `${customerId}-1`, total: 40 }];
}
/** @param {string} id */
export async function loadDashboard(id) {
const jobs = [loadCustomer(id), loadOrders(id)];
const [customer, orders] = await Promise.all(jobs);
return { name: customer.name, count: orders.length };error TS2339: Property 'name' does not exist on type 'Customer | Order[]'.
Property 'name' does not exist on type 'Order[]'.error TS2339: Property 'length' does not exist on type 'Customer | Order[]'.
Property 'length' does not exist on type 'Customer'.
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": ["node"]
},
"include": ["*.js"]
} Pass the literal straight to Promise.all, or give the variable a tuple type: /** @type {[Promise<Customer>, Promise<Order[]>]} */.
Promise.allSettled never rejects; each result says whether its promise was fulfilled or rejected. The result is a union of two shapes, and only the fulfilled one has value:
/** @param {string} url @returns {Promise<number>} */
async function ping(url) {
const started = Date.now();
await fetch(url, { method: "HEAD" });
return Date.now() - started;
}
const results = await Promise.allSettled([ping("https://example.com/"), ping("https://example.org/")]);
for (const result of results) {
console.log(result.value);error TS2339: Property 'value' does not exist on type 'PromiseSettledResult<number>'.
Property 'value' does not exist on type 'PromiseRejectedResult'.
if (result.status === "fulfilled") console.log(`${result.value} ms`);
else console.log(`failed: ${result.reason}`);
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": ["node"]
},
"include": ["*.js"]
} Checking result.status === "fulfilled" narrows the union, so result.value is a number in that branch and result.reason is available in the other.
Gotchas
reasonisany. A rejection can carry anything, so check it (reason instanceof Error) before readingmessage./** @type {const} */works too. Casting the array with/** @type {const} */ ([loadCustomer(id), loadOrders(id)])keeps it a tuple, the JSDoc form ofas const.- Order follows the array, not completion. The results come back in the order of the input array, whichever promise settles first.
Questions
Do I need any JSDoc for Promise.all?
Usually not. Type the functions that return the promises; Promise.all keeps their types position by position as long as it receives an array literal.
What does Promise.race return?
A promise of the union of the input types: racing a Promise<Customer> and a Promise<Order[]> gives Promise<Customer | Order[]>, because either one may win.
Sources
- Promise.all() · MDN Web Docs
- Promise.allSettled() · MDN Web Docs
- Narrowing (discriminated unions) · TypeScript handbook
Last reviewed 21 October 2026.