Skip to content
TypeScript ts async 4 min read

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.

CombinatorResolves toOn rejection
Promise.alltuple [T1, T2, ...]rejects on first failure
Promise.allSettledarray of result objectsnever rejects (captures all)
Promise.racefirst settled value Trejects if first to settle rejects
Promise.anyfirst fulfilled value Trejects 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 T in Promise<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.all for fail-fast concurrency and Promise.allSettled when partial success is acceptable.
  • Narrow PromiseSettledResult on its status discriminant before reading value or reason.
  • Never assume a rejection reason’s type — treat it as unknown and narrow.
Last updated June 29, 2026
Was this helpful?