Skip to main content

TS Ep 45: Inferring Promises (`Awaited`)

Rachmat Hidayat
Author
Rachmat Hidayat
Learn & sharing insights on TypeScript, Go, Kubernetes, DevOps, DevSecOps, SRE, Platform Engineering, AI/ML Engineering, and MLOps.
typescript - This article is part of a series.
Part 45: This Article
In asynchronous JavaScript, a 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>>>:

  1. Iteration 1:
    • Does Promise<Promise<string>> match Promise<infer InnerType>?
    • Yes. InnerType is inferred as Promise<string>.
    • The condition returns: CustomAwaited<Promise<string>>.
  2. Iteration 2:
    • Does Promise<string> match Promise<infer InnerType>?
    • Yes. InnerType is inferred as string.
    • The condition returns: CustomAwaited<string>.
  3. Iteration 3:
    • Does string match Promise<infer InnerType>?
    • No.
    • The condition returns the raw T: string.

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 the await keyword in Type Space.
  • We combined infer with 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!

typescript - This article is part of a series.
Part 45: This Article