Typing Promises & async/await
Promise<T> is TypeScript's type for an asynchronous value that arrives later; await unwraps it to a T , every async function returns a Promise, and Awaited<T> peels those Promise layers off in the type system.
Learn Typing Promises & async/await in our free TypeScript course — an interactive lesson with runnable examples, a practice exercise and a quick reference.
Part of the free TypeScript course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
A Promise<T> is like a coat-check ticket. You hand over your coat and get a ticket (the promise) immediately — but the ticket isn't the coat. await is redeeming the ticket later to get the actual coat (the T ) back. An async function always hands you a ticket, never the coat directly, which is why forgetting to await leaves you holding a ticket where you expected a value.
1. Promise<T> and Unwrapping with await
Any function that resolves a value later is typed to return a Promise<T> , where T is the value it eventually produces. Inside an async function, await pauses until the promise settles and hands you the unwrapped T — so await fetchUser(7) turns a into a User .
2. Async Return Types & Awaited<T>
Marking a function async means its return type is always wrapped in a Promise: return 42 gives . To go the other way in the type system — from a Promise type to the value it resolves to — use the built-in Awaited<T> , which even flattens nested promises.
3. A Typed Fetch Wrapper & Promise.all Tuples
A generic wrapper lets the call site say what shape it expects — resolves to a User . And when you await a fixed tuple via Promise.all , TypeScript keeps each position's type, so destructuring gives you correctly typed variables instead of a widened union.
🎯 Your Turn
Unwrap a promise from a tiny fetch-style function. Fill in the two blanks marked ___ , then run it.
No blanks this time — just a brief and a starting outline. Build the Promise.all pipeline yourself, run it, and check your output against the example in the comments.
Practice quiz
What does Promise<User> represent?
- A User that exists right now
- An array of Users
- An async operation that will resolve to a User later
- A User that may be null
Answer: An async operation that will resolve to a User later. Promise<T> is the type of an async operation that will eventually resolve to a value of type T - here, a User.
Inside an async function, what does await fetchUser(7) give you when fetchUser returns Promise<User>?
- User
- Promise<User>
- Promise<Promise<User>>
- unknown
Answer: User. await unwraps a Promise<T> to its inner value T, so awaiting a Promise<User> yields a User.
An async function does return 42. What is its return type?
- number
- 42
- Promise<void>
- Promise<number>
Answer: Promise<number>. Every async function wraps its return value in a Promise, so returning a plain number gives the type Promise<number>.
What does the built-in Awaited<T> utility type do?
- Wraps T in a Promise
- Recursively unwraps Promise layers to the resolved value type
- Makes T optional
- Converts T to an array
Answer: Recursively unwraps Promise layers to the resolved value type. Awaited<T> peels off Promise layers (even nested ones) to give the final resolved value type, mirroring what await does.
What is Awaited<Promise<Promise<string>>>?
- string
- Promise<string>
- Promise<Promise<string>>
- unknown
Answer: string. Awaited fully flattens nested promises, so Awaited<Promise<Promise<string>>> resolves to string.
What is Awaited<number> (a non-promise type)?
- Promise<number>
- never
- number
- void
Answer: number. Awaited is a no-op on non-promise types, so Awaited<number> is just number.
What type does await Promise.all([Promise.resolve("a"), Promise.resolve(1)]) produce?
Promise.all over a fixed tuple resolves to a tuple keeping each position's type: [string, number].
Why does reading user.name on const user = fetchUser(1) (no await) fail to type-check?
- fetchUser doesn't exist
- name is private
- fetch returns void
- user is a Promise<User>, not a User
Answer: user is a Promise<User>, not a User. Without await, user is a Promise<User>, which has no .name property - a very common bug. Add await first.
Why can't you use await directly in a non-async function?
- await only works at the top level
- await is only allowed inside async functions (or top-level module await)
- await requires a try/catch
- await needs a Promise.all
Answer: await is only allowed inside async functions (or top-level module await). 'await' expressions are only allowed within async functions (or as top-level await in a module). Mark the function async.
How do you grab the resolved result type of an async function fn for reuse?
- ReturnType<typeof fn>
- Promise<typeof fn>
- Awaited<ReturnType<typeof fn>>
- typeof fn.return
Answer: Awaited<ReturnType<typeof fn>>. ReturnType<typeof fn> is the Promise; wrapping it in Awaited<...> unwraps it to the resolved value type.