Promise might resolve to another Promise. At runtime, the await keyword automatically pauses and recursively unwraps these nested promises until it reaches raw data. In Type Space, we achieve this using the Awaited<T> utility type.1. The Awaited<T> Utility#
TypeScript provides a built-in utility called Awaited<T> that perfectly mirrors the runtime behavior of await. It takes a Promise type and extracts the underlying payload.
Crucially, if it encounters a Promise wrapped inside another Promise, it will recursively unwrap them all.
type P1 = Promise<string>;
type P2 = Promise<Promise<number>>;
type P3 = Promise<Promise<Promise<boolean>>>;
// 🟢 Unwraps a single layer
type Unwrapped1 = Awaited<P1>; // string
// 🟢 Unwraps multiple layers recursively!
type Unwrapped2 = Awaited<P2>; // number
type Unwrapped3 = Awaited<P3>; // boolean
2. Rebuilding Awaited (Type-Level Recursion)#
How does the TypeScript compiler actually achieve this infinite unwrapping?
It combines the infer keyword (from Episode 44) with Recursive Type Aliases (a type that calls itself).
Let’s rebuild Awaited from scratch to understand the mechanics:
// 1. Is T a Promise? We check using pattern matching: T extends Promise<infer Inner>
// 2. If YES: Pass 'Inner' back into MyAwaited (Recursion!)
// 3. If NO: It must be raw data. Return T as the final result.
type CustomAwaited<T> =
T extends Promise<infer InnerType>
? CustomAwaited<InnerType>
: T;Tracing the Recursion#
Let’s trace the execution of CustomAwaited<Promise<Promise<string>>>:
- Iteration 1:
- Does
Promise<Promise<string>>matchPromise<infer InnerType>? - Yes.
InnerTypeis inferred asPromise<string>. - The condition returns:
CustomAwaited<Promise<string>>.
- Does
- Iteration 2:
- Does
Promise<string>matchPromise<infer InnerType>? - Yes.
InnerTypeis inferred asstring. - The condition returns:
CustomAwaited<string>.
- Does
- Iteration 3:
- Does
stringmatchPromise<infer InnerType>? - No.
- The condition returns the raw
T:string.
- Does
Final Result: string.
(Note: The actual implementation of Awaited in the TypeScript standard library is slightly more complex to handle “Thenable” objects, but the recursive infer logic is exactly the same!)
3. Real-World Architecture: Async Return Types#
A highly common advanced pattern in full-stack TypeScript is extracting the resolved payload from a backend async function to share with the frontend UI components.
If you use ReturnType<typeof fetchUser>, you will receive the un-awaited type: Promise<User>. To get the raw User type, you must compose Awaited and ReturnType together:
// Backend fetcher function
async function fetchUserFromDB(id: string) {
// ... database logic
return { id, name: "Alice", lastLogin: new Date() };
}
// 1. ReturnType extracts: Promise<{ id: string, name: string, lastLogin: Date }>
// 2. Awaited unwraps it!
type UserData = Awaited<ReturnType<typeof fetchUserFromDB>>;
/* Inferred Type:
{
id: string;
name: string;
lastLogin: Date;
}
*/
// The frontend component can now use the extracted type!
function UserProfile({ data }: { data: UserData }) {
return <div>{data.name}</div>;
}By heavily utilizing Awaited<ReturnType<typeof fn>>, you can build “backend-driven types” where your database queries act as the single source of truth for your entire application’s data models!
Summary & Next Steps#
In this episode:
- We used
Awaited<T>to simulate theawaitkeyword in Type Space. - We combined
inferwith Recursive Types to build an infinite Promise unwrapper. - We traced the compiler’s execution path through recursive conditional layers.
- We extracted raw data payloads from async functions using
Awaited<ReturnType<T>>.
Promises aren’t the only structures we can unwrap with infer.
In Episode 46: Infer with Tuples and Arrays, we will learn how to extract the first, last, or remaining elements of an array, unlocking type-level array manipulation!

