10LOC
TypeScriptintermediate

Awaited<T> and unwrapping nested promise types

Published April 6, 2027

type Nested = Promise<Promise<Promise<string>>>;
type Flat = Awaited<Nested>;

export async function loadUser(): Promise<{ id: string; name: string }> {
  return { id: "1", name: "Ada" };
}

export type LoadedUser = Awaited<ReturnType<typeof loadUser>>;

export const greet = (user: LoadedUser): string => `Hello, ${user.name}`;

export const flatValue: Flat = "done";

What

Awaited<T> is a built-in TypeScript utility type that unwraps however many layers of Promise (or any thenable) surround a type, down to the value that's actually left once every layer resolves — mirroring what await does to a value at runtime.

Why it matters

Before Awaited shipped (TypeScript 4.5, driven by Promise.all's type getting more accurate), unwrapping a promise type meant a hand-rolled conditional type, and it was easy to get subtly wrong for the nested case: Promise<Promise<T>> is a real type you can end up with — for example, an async function that returns another async function's still-pending promise — and a naive T extends Promise<infer U> ? U : T only strips one layer, leaving Promise<T> instead of T. Awaited recurses until there's nothing left to unwrap, matching the actual runtime behavior of await, which also keeps awaiting until it gets a non-thenable value.

How it works

  • Flat, from Awaited<Nested> where Nested is Promise<Promise<Promise<string>>>, resolves straight to string — all three layers are gone, not just one.
  • LoadedUser composes Awaited with ReturnType: ReturnType<typeof loadUser> gets the async function's declared return type, Promise<{ id: string; name: string }>, and Awaited unwraps that down to the plain object. This combination is the actual common case — you usually want "what this async function resolves to," not "what promise type it returns."
  • greet takes LoadedUser directly, as if loadUser had never been async in the first place, which is the point: Awaited<ReturnType<...>> is what lets calling code work in resolved types when the promise layer is just plumbing.

Gotchas

  • Awaited unwraps thenables generally, not only Promise. Any object with a .then method participates in the same unwrapping, matching how await treats them at runtime too.
  • ReturnType<typeof loadUser> alone, without Awaited, is Promise<{ id: string; name: string }> — a very common half-step that type-checks but is rarely what you actually want to store or pass around.

Related

  • Conditional types + inferAwaited is implemented with exactly this mechanism; the AsyncReturnType pattern is a simplified, one-level-deep version of what Awaited does generally.
  • Promise.all's return type — the motivating use case for Awaited's addition, unwrapping a tuple of promises into a tuple of their resolved values.