Promise Types
Promises are the backbone of asynchronous JavaScript, and TypeScript models them with the generic Promise<T>, where T is the value the promise eventually resolves to. Getting the generic argument right means every .then, await, and combinator downstream stays fully typed. This page explains how to read and write Promise<T>, how async functions are guaranteed to return promises, how the Awaited<T> utility unwraps even nested promises, and how to type the concurrency combinators Promise.all and Promise.allSettled precisely.
The Promise generic
A Promise<T> is a placeholder for a value of type T that will be available later. The type parameter describes the resolved value; rejection reasons are intentionally untyped (always any/unknown at the catch site) because JavaScript lets you throw anything.
const ready: Promise<number> = Promise.resolve(42);
ready.then((value) => {
// value is inferred as number
console.log(value.toFixed(2));
});
// A promise that resolves to nothing useful uses Promise<void>
const done: Promise<void> = fetch("/ping").then(() => undefined);
The callback passed to .then receives a parameter typed as T, so value above is a number with full method autocomplete. Use Promise<void> when a promise is awaited only for its side effect and produces no meaningful value.
Async functions always return a promise
Any function marked async returns a Promise, no matter what you write inside it. If you return a plain value of type T, the function’s type is Promise<T>; a bare return or no return gives Promise<void>. Returning an existing promise does not double-wrap it.
async function getCount(): Promise<number> {
return 5; // wrapped automatically -> Promise<number>
}
async function loadUser(id: string) {
const res = await fetch(`/api/users/${id}`);
return res.json(); // inferred Promise<any> until you type it
}
// ❌ Error: a non-async function cannot declare Promise via a plain return
function broken(): Promise<number> {
return 5; // Type 'number' is not assignable to type 'Promise<number>'
}
Annotate the return type explicitly (: Promise<number>) on exported async functions so the contract is documented and a wrong return is caught inside the function body rather than at the call site.
Unwrapping with Awaited
The built-in Awaited<T> utility recursively unwraps a promise to the type you would get from await. It collapses nested promises and thenables, mirroring the runtime behaviour where await flattens automatically.
type A = Awaited<Promise<string>>; // string
type B = Awaited<Promise<Promise<number>>>; // number
type C = Awaited<boolean>; // boolean (non-promise passes through)
async function getUser() {
return { id: 1, name: "Ada" };
}
// Extract the resolved type from an async function's return type
type User = Awaited<ReturnType<typeof getUser>>; // { id: number; name: string }
Awaited<T> is the correct way to derive the resolved type from an async function or any promise-returning API. Pairing it with ReturnType<typeof fn> lets a consumer reuse the exact shape a function resolves to without manually duplicating the type.
Before TS 4.5, people wrote brittle helpers to unwrap promises. Always reach for the built-in
Awaited<T>now — it handles recursion and thenables correctly.
Typing Promise.all
Promise.all takes an iterable of promises and resolves to a tuple of their resolved values, preserving order and individual types. When you pass a fixed array literal, TypeScript infers a precise tuple rather than a widened union array.
async function loadDashboard() {
const [count, name, active] = await Promise.all([
Promise.resolve(10), // number
Promise.resolve("Ada"), // string
Promise.resolve(true), // boolean
]);
// count: number, name: string, active: boolean
return { count, name, active };
}
The tuple inference is what makes destructuring safe: each element keeps its own type instead of collapsing to number | string | boolean. If any promise rejects, Promise.all rejects immediately with that reason and the others are abandoned.
Promise.all vs allSettled vs race
The combinators differ in how they handle failures and what they resolve to. Choose based on whether one failure should abort everything.
| Combinator | Resolves to | On rejection |
|---|---|---|
Promise.all | tuple [T1, T2, ...] | rejects on first failure |
Promise.allSettled | array of result objects | never rejects (captures all) |
Promise.race | first settled value T | rejects if first to settle rejects |
Promise.any | first fulfilled value T | rejects only if all reject |
Promise.allSettled resolves to PromiseSettledResult<T>[], a discriminated union you narrow on the status field.
const results = await Promise.allSettled([
fetchUser("1"),
fetchUser("2"),
]);
for (const result of results) {
if (result.status === "fulfilled") {
console.log(result.value); // typed as the resolved value
} else {
console.error(result.reason); // unknown
}
}
Narrowing on status === "fulfilled" exposes result.value; the "rejected" branch exposes result.reason, which is any/unknown since rejection reasons are not statically known.
Best Practices
- Always annotate
TinPromise<T>; let it default only for trivial inline cases. - Add explicit
: Promise<T>return types to exported async functions to document the contract. - Use
Promise<void>for promises awaited purely for side effects. - Derive resolved types with
Awaited<ReturnType<typeof fn>>instead of duplicating shapes. - Prefer
Promise.allfor fail-fast concurrency andPromise.allSettledwhen partial success is acceptable. - Narrow
PromiseSettledResulton itsstatusdiscriminant before readingvalueorreason. - Never assume a rejection reason’s type — treat it as
unknownand narrow.