Utility type awaited in TypeScript
Awaited
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 withReturnTypefor the full pattern. - Available since TypeScript 4.5.
Quick example
// 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 tostring | number. Promise.alltyping: 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:
type Fn = () => Promise<number>;
type Wrong = Awaited<Fn>; // never
type Right = Awaited<ReturnType<Fn>>; // numberA 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:
// 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 - correctThe manual version does not recurse. If you see this in a legacy codebase, replace it with Awaited.
Forgetting ReturnType when working with async functions:
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:
type Obj = { data: Promise<string> };
type T = Awaited<Obj>; // Obj - unchangedAwaited 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:
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
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>>; // UserReturnType 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
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
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 | voidAwaited<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 readyA concise answer to help you respond confidently on this topic during an interview.