Skip to content
ExponaBLOG
JAVASCRIPT & TYPESCRIPT · DAY 069 / 100

The Awaited type for JavaScript & TypeScript developers

Get the resolved type of any promise.

Expona Admin6 min read

The Awaited type

When you're working with promises and async functions in TypeScript, you often need to know what type you'll actually get after the promise resolves. The Awaited type does exactly that—it unwraps promises to give you the resolved value's type. This utility has become essential for building type-safe async code, especially when dealing with complex promise chains or frameworks that return deeply nested promise structures.

Understanding the basics

Let me start with the most straightforward use case. Imagine you have a promise that resolves to a User object:

typescript
interface User {
  id: number;
  name: string;
  email: string;
}

const userPromise: Promise<User> = fetchUser(1);

Now, if I asked you "what type do you get when you await this promise?", the answer is User. That's exactly what Awaited does. It takes a promise type and extracts the resolved type:

typescript
type ResolvedUser = Awaited<Promise<User>>;
// ResolvedUser is User

The syntax is simple: Awaited<T> where T is a promise-like type. But here's the power—it doesn't just work with Promise. It works with any thenable (anything with a .then() method), which makes it incredibly flexible when dealing with different async libraries or custom promise implementations.

In practical work, I use Awaited when I need to define a type based on what a promise will give me, without manually writing out that type again. It's a single source of truth. If the promise type changes upstream, my derived type updates automatically.

Nested promises and recursive unwrapping

Real-world async code isn't always simple. Sometimes you end up with promises that resolve to promises that resolve to something else. This is where Awaited really shines.

Let's say you have a function that returns a promise of a promise:

typescript
type NestedPromise = Promise<Promise<string>>;

type Unwrapped = Awaited<NestedPromise>;
// Unwrapped is string, not Promise<string>

Notice that Awaited recursively unwraps nested promises until it reaches the final resolved type. This is crucial because it matches JavaScript's behavior—when you await a promise that resolves to another promise, JavaScript will automatically await that nested promise too.

I've seen this pattern in real APIs. For example, some middleware or interceptor libraries might wrap responses in additional promise layers. Here's a more realistic scenario from my work:

typescript
// A caching layer that wraps an async operation
async function getCachedUser(id: number): Promise<Promise<User>> {
  const cached = await cache.get(`user:${id}`);
  if (cached) {
    return Promise.resolve(cached);
  }
  return fetchUser(id);
}

// With Awaited, we extract the final type
type FinalUserType = Awaited<ReturnType<typeof getCachedUser>>;
// FinalUserType is User

Without Awaited, you'd have to manually track through the nesting levels. With it, you're always guaranteed the actual resolved type, no matter how many promise layers exist.

Combining Awaited with ReturnType for async functions

One of my favorite patterns is using Awaited with ReturnType to extract the resolved type from async functions. Async functions always return promises, so this combination is powerful.

Here's the pattern:

typescript
async function getUserWithPosts(id: number) {
  const user = await fetchUser(id);
  const posts = await fetchUserPosts(id);
  return { user, posts };
}

type UserWithPostsType = Awaited<ReturnType<typeof getUserWithPosts>>;
// UserWithPostsType is { user: User; posts: Post[] }

Let me break this down. ReturnType<typeof getUserWithPosts> gives you Promise<{ user: User; posts: Post[] }>. Then Awaited unwraps it to the actual object type. This is invaluable when you want consumers to use the resolved type without knowing the async details.

In a CRM platform I built, we had many helper functions like this. Instead of duplicating type definitions, we extracted them automatically:

typescript
// Helper function
async function getCustomerProfile(customerId: string) {
  const customer = await db.customer.findUnique({ where: { id: customerId } });
  const interactions = await db.interaction.findMany({ where: { customerId } });
  const opportunities = await db.opportunity.findMany({ where: { customerId } });
  
  return { customer, interactions, opportunities };
}

// Use the actual type everywhere
type CustomerProfile = Awaited<ReturnType<typeof getCustomerProfile>>;

// In API responses
app.get('/customer/:id', async (req, res) => {
  const profile: CustomerProfile = await getCustomerProfile(req.params.id);
  res.json(profile);
});

This keeps everything in sync. If you add a field to the return value, every place that uses CustomerProfile automatically knows about it.

How Promise.all uses Awaited

TypeScript's built-in typings for Promise.all use Awaited under the hood. Understanding this helps you write correct concurrent code.

When you use Promise.all, TypeScript needs to figure out the types of all resolved values. Here's how it works:

typescript
const userPromise = fetchUser(1);
const postsPromise = fetchUserPosts(1);
const settingsPromise = fetchSettings();

const [user, posts, settings] = await Promise.all([
  userPromise,
  postsPromise,
  settingsPromise
]);

// TypeScript infers:
// user: User
// posts: Post[]
// settings: Settings

Under the hood, Promise.all's type definition uses Awaited to transform each promise in the array into its resolved type. Without it, you'd get back [Promise<User>, Promise<Post[]>, Promise<Settings>], which wouldn't be correct.

In my e-commerce platform, I frequently load multiple data sources in parallel:

typescript
async function getProductPage(productId: string) {
  const [product, reviews, recommendations, inventory] = await Promise.all([
    db.product.findUnique({ where: { id: productId } }),
    db.review.findMany({ where: { productId } }),
    getRecommendations(productId),
    redis.get(`inventory:${productId}`)
  ]);

  return { product, reviews, recommendations, inventory };
}

type ProductPageData = Awaited<ReturnType<typeof getProductPage>>;

The Awaited type ensures that when you return from Promise.all`, TypeScript knows you have the resolved types, not promises anymore.

Common mistakes

Mistake 1: Forgetting to unwrap when you need the actual type. A common error is accessing properties on what you think is the resolved type but is still wrapped in a promise:

typescript
type UserPromiseType = Promise<User>;
// Wrong: this is still a promise
type Wrong = UserPromiseType extends Promise<infer U> ? U : never;

// Right: use Awaited
type Right = Awaited<UserPromiseType>;

Mistake 2: Not recognizing when you need Awaited vs when you don't. If you're inside an async function and already working with the awaited value, you don't need Awaited. It's for type extraction, not runtime unwrapping:

typescript
// Don't need Awaited here—you're already working with User
async function processUser(id: number) {
  const user = await fetchUser(id);
  // user is already User, not Promise<User>
  console.log(user.name);
}

Mistake 3: Over-using Awaited in function parameters. You don't need Awaited in function signatures unless you're doing advanced type-level programming. Declare what you expect to receive directly:

typescript
// Good: clear what you're accepting
function displayUser(user: User) {
  console.log(user.name);
}

// Overly complex: don't do this
function displayUser(user: Awaited<Promise<User>>) {
  console.log(user.name);
}

Key takeaways

  • Awaited<T> extracts the resolved type from any promise-like type, giving you what you'd get after awaiting it
  • It recursively unwraps nested promises, so Awaited<Promise<Promise<string>>> correctly gives you string
  • Combine Awaited with ReturnType to automatically extract types from async functions without manual duplication
  • TypeScript's Promise.all typings use Awaited internally to give you proper resolved types in the tuple result
  • Use Awaited for type-level programming and type extraction; don't use it for runtime promise handling
  • Keep your types DRY by deriving them from existing functions rather than manually redefining them
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles