Skip to main content

Utility type awaited in TypeScript

Awaited is a TypeScript utility type, available since v4.5, that recursively unwraps Promise chains and returns the final resolved value type.

Theory

TL;DR

  • Analogy: each Promise is a wrapper box. Awaited opens every box, no matter how many layers deep, and gives you what is inside.
  • Core behavior: unwraps single Promises, nested Promises, and union types. Returns T unchanged if T is not a Promise.
  • Decision rule: use Awaited<ReturnType<typeof fn>> to type async function results; combine with ReturnType for the full pattern.
  • Available since TypeScript 4.5.

Quick example

typescript
// Single layer type A = Awaited<Promise<string>>; // string // Nested - TypeScript recurses automatically type B = Awaited<Promise<Promise<number>>>; // number // Union - distributes over each member type C = Awaited<Promise<string> | number>; // string | number // Practical: async function return type async function fetchUser(): Promise<{ id: number }> { return { id: 42 }; } type UserType = Awaited<ReturnType<typeof fetchUser>>; // { id: number }

TypeScript strips each Promise layer one by one until it reaches a plain type. The union case is worth noting: Awaited<Promise<A> | B> becomes A | B, not Promise<A | B>.

How recursive unwrapping works

TypeScript implements Awaited as a conditional type: if T extends Promise<infer U>, recurse on Awaited<U>; else return T. This happens entirely at compile time. No runtime object, no overhead. V8 and Node never see it.

Union distribution is built in. Awaited<Promise<A> | Promise<B>> resolves to A | B because TypeScript maps the conditional over each union member separately. That makes it safe to use with API functions that return Promise<ResponseA> | Promise<ResponseB> depending on a parameter.

I have seen this trip up developers working with older SDK wrappers that return Promise<Promise<T>> due to double-wrapping. Awaited handles it without any extra work.

When to use

  • Async function returns: Awaited<ReturnType<typeof fn>> gives you the resolved type without manual assertions.
  • API chains: when Promise depth is unknown or varies across SDK versions.
  • Union of async results: Awaited<Promise<string> | Promise<number>> collapses to string | number.
  • Promise.all typing: pair with a mapped type over a tuple to unwrap each element.

Skip it when T is already a plain type. Awaited<string> just returns string. Correct, but pointless.

Common mistakes

Applying Awaited to a function type instead of its return type:

typescript
type Fn = () => Promise<number>; type Wrong = Awaited<Fn>; // never type Right = Awaited<ReturnType<Fn>>; // number

A function type does not extend Promise<infer U>, so Awaited falls through to never. Always wrap with ReturnType first.

Manual single-layer unwrapping that fails on nested Promises:

typescript
// Old polyfill pattern (pre-4.5) type OldWay<T> = T extends Promise<infer U> ? U : T; type A = OldWay<Promise<Promise<string>>>; // Promise<string> - still wrapped type B = Awaited<Promise<Promise<string>>>; // string - correct

The manual version does not recurse. If you see this in a legacy codebase, replace it with Awaited.

Forgetting ReturnType when working with async functions:

typescript
async function loadData() { return { id: 1, name: "Alice" }; } type T1 = ReturnType<typeof loadData>; // Promise<{ id: number; name: string }> type T2 = Awaited<ReturnType<typeof loadData>>; // { id: number; name: string }

Expecting Awaited to unwrap Promise fields inside objects:

typescript
type Obj = { data: Promise<string> }; type T = Awaited<Obj>; // Obj - unchanged

Awaited only touches the top-level Promise. It does not recurse into object shapes. You need a recursive mapped type for that.

Real-world usage

  • TanStack Query: type Data = Awaited<ReturnType<typeof fetchUser>> for typed hook data without manual assertions.
  • tRPC: inferRouterOutputs<AppRouter> uses Awaited internally to type all procedure results.
  • Express async handlers: Awaited<ReturnType<typeof handler>> for middleware return type inference.
  • Zod with async transforms: when a schema has an async transform step, Awaited resolves the output type correctly.

Follow-up questions

Q: What does Awaited<Promise<void>> resolve to?
A: void. TypeScript does not collapse void to never here. That is the correct behavior for fire-and-forget functions. Awaiting them gives you void, and Awaited reflects that.

Q: How does Awaited distribute over union types?
A: Awaited<Promise<A> | B> becomes Awaited<Promise<A>> | Awaited<B>, which is A | B. TypeScript maps the conditional over each union member separately.

Q: What is the difference between Awaited and the old manual unwrap pattern?
A: The manual pattern T extends Promise<infer U> ? U : T unwraps exactly one layer. Awaited recurses until it hits a non-Promise type and also handles union distribution correctly.

Q: Implement Awaited manually without the built-in type.
A:

typescript
type MyAwaited<T> = T extends Promise<infer U> ? MyAwaited<U> : T;

This covers the recursion. TypeScript handles union distribution automatically for distributive conditional types.

Q: In React Query, why use Awaited<ReturnType<typeof fn>> instead of typing data manually?
A: Because it tracks the queryFn signature automatically. Change the function return type and the hook data type updates with it. A manual annotation can go stale without any compiler warning.

Examples

Basic: extracting a resolved type from an async function

typescript
interface User { id: number; name: string; } async function fetchUser(id: number): Promise<User> { const res = await fetch(`/api/users/${id}`); return res.json(); } // ReturnType alone - still wrapped in Promise type Wrapped = ReturnType<typeof fetchUser>; // Promise<User> // Add Awaited - Promise layer removed type Resolved = Awaited<ReturnType<typeof fetchUser>>; // User

ReturnType extracts whatever the function returns, Promise included. Awaited strips the wrapper. The combination is the standard pattern for typing async results in production code.

Typing a React Query hook

typescript
interface Post { id: number; title: string; } async function fetchPost(id: number): Promise<Post> { const res = await fetch(`/api/posts/${id}`); return res.json(); } type PostData = Awaited<ReturnType<typeof fetchPost>>; // Post function usePost(id: number) { return useQuery({ queryKey: ["post", id], queryFn: () => fetchPost(id), // data is inferred as Post | undefined automatically }); }

TanStack Query v5 infers the data type from queryFn using Awaited internally. You get Post | undefined on data without writing a single type annotation on the hook itself.

Edge case: union with void

typescript
type Complex = Promise<Promise<string> | void>; type Resolved = Awaited<Complex>; // Step 1: Awaited<Promise<string> | void> // Step 2: Awaited<Promise<string>> | Awaited<void> // Step 3: string | void

Awaited<void> stays void, not never. If you need to remove void from the result, use Exclude<Resolved, void>, which gives string. Those are two separate operations. Awaited does not do both.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?