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:
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:
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:
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:
// 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:
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:
// 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:
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:
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:
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:
// 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:
// 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 youstring - Combine
AwaitedwithReturnTypeto automatically extract types from async functions without manual duplication - TypeScript's
Promise.alltypings useAwaitedinternally to give you proper resolved types in the tuple result - Use
Awaitedfor 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






Comments
Loading comments…