In TypeScript, why does `ReturnType<typeof fetchUser>` not give you the user object when `fetchUser` is an `async` function, and what does `Awaited<T>` do about it?
answer
- async means the signature returns a promise
- one more step after the return type
- unwrapping recurses, not once
- non-promise types pass through
- unions unwrap member by member
basics
~10 sAn async function's return type is Promise<T>, so ReturnType gives Promise<{...}>, not the object. Awaited<T> unwraps it: Awaited<ReturnType<typeof fetchUser>> is the user shape. Awaited recurses through nested promises and passes non-promise types through unchanged.
solid answer
~40 sAn `async` function's declared return type is always a promise, so `ReturnType<typeof fetchUser>` is `Promise<{ id: string; name: string }>` — correct, just not the shape you wanted. `Awaited<T>` models what `await` would produce at the type level, so `Awaited<ReturnType<typeof fetchUser>>` gives the user object. Two behaviours matter. It recurses: `Awaited<Promise<Promise<number>>>` is `number`, matching the fact that awaiting a promise of a promise settles all the way down. And it is a pass-through for non-thenables — `Awaited<string>` is `string` — which is why it composes safely over a generic that might or might not be a promise. It also distributes across a union, so `Awaited<Promise<string> | number>` is `string | number`.
code
typescript · 16 linesasync function fetchUser(id: string) {
return { id, name: "Ada" };
}
type Raw = ReturnType<typeof fetchUser>; // Promise<{ id: string; name: string }>
type User = Awaited<Raw>; // { id: string; name: string }
type Nested = Awaited<Promise<Promise<number>>>; // number (recursive)
type Plain = Awaited<string>; // string (pass-through)
type Mixed = Awaited<Promise<string> | number>; // string | number (distributes)
const u: User = { id: "1", name: "Ada" };
const n: Nested = 42;
const p: Plain = "still a string";
const m: Mixed = 7;
console.log(u, n, p, m);go deeper
Remember that async makes the return type a promise, and that Awaited<ReturnType<typeof fn>> is the pair you write to get the resolved shape.
Explain the three behaviours — recursive unwrapping, pass-through for non-thenables, and distribution over unions — and why they make a hand-written unwrapper unnecessary.
Use it to derive one canonical shape per data-access function and to type helpers that accept sync or async callbacks, while keeping clear that no runtime awaiting is implied.
Weigh whether resolved shapes across the codebase should be derived from fetchers or declared as contracts the fetchers must satisfy, since the derived direction lets a data-layer change ripple outward unannounced.
## The mismatch ```ts async function fetchUser(id: string) { return { id, name: "Ada" }; } type A = ReturnType<typeof fetchUser>; // Promise<{ id: string; name: string }> ``` Marking a function `async` means its return type is a promise of whatever the body returns — that is the signature, and `ReturnType` reports it accurately. What you usually want when you write `type User = ...` is the **settled** value, and that is one more step: ```ts type User = Awaited<ReturnType<typeof fetchUser>>; // { id: string; name: string } ``` This two-utility composition is one of the most common idioms in application code, because it lets a data-fetching function be the single source of truth for the shape it produces. ## What `Awaited` models `Awaited<T>` answers the question "what would `await`ing a value of type `T` give me?" — at the type level only, with no runtime effect. Three behaviours follow from that framing, and they are exactly what interviewers probe: **It recurses.** Awaiting a promise that resolves to another promise settles all the way down, so the type does too: ```ts type N = Awaited<Promise<Promise<number>>>; // number ``` A naive hand-written unwrapper that matches `Promise<infer U>` once would stop at `Promise<number>`. `Awaited` applies itself again. **It passes non-thenables through.** `await 5` is just `5`, and the type mirrors that: ```ts type S = Awaited<string>; // string ``` This is what makes `Awaited` safe to apply blindly. In a generic helper where `T` might be `User` or `Promise<User>`, `Awaited<T>` is correct either way — you do not need a conditional to decide whether to unwrap. **It distributes over unions.** A union is unwrapped member by member: ```ts type U = Awaited<Promise<string> | number>; // string | number ``` That is the same distribution rule every conditional type over a naked type parameter follows, and it is what you want here: a value that is either a promise of a string or a plain number settles to a string or a number. ## Thenables, not just `Promise` `Awaited` is written against the *thenable* structure — an object with a `then` method taking a callback — not literally against the `Promise` class. That matters when you deal with a library that returns its own promise-like type: a query builder, an older deferred implementation, a lazily-awaitable handle. If the type is structurally thenable, `Awaited` unwraps it too, which is why it is preferred over any home-grown `UnwrapPromise` alias people used to write. ## Where it shows up in real code ```ts // One derived type for the whole app's user shape: type User = Awaited<ReturnType<typeof fetchUser>>; // One element of a list endpoint's result: type Row = Awaited<ReturnType<typeof listOrders>>[number]; // A generic helper that does not care whether the callback is async: declare function runOnce<T>(fn: () => T): Promise<Awaited<T>>; ``` That last pattern is the one to have ready: a helper accepting either a sync or an async callback declares its result as `Promise<Awaited<T>>`, which is right in both cases without an overload or a conditional. ## The boundary worth stating All of this is a compile-time model of settling. `Awaited<T>` inserts no `await`, adds no microtask, and cannot make a synchronous value asynchronous or vice versa — the types are erased. If your code forgot the actual `await`, the checker will complain that a `Promise<User>` is not assignable to a `User`; `Awaited` on the type side is not a substitute for the keyword on the value side. The runtime settling rules themselves — job queues, chaining, thenable assimilation — are JavaScript's story, and `Awaited` is simply the type layer's faithful shadow of them.
- What is `Awaited<Promise<Promise<number>>>`, and why does a hand-written single-step unwrapper get it wrong?It is `number`. `Awaited` applies itself recursively, mirroring the fact that awaiting a promise of a promise settles all the way down to the innermost value. A hand-rolled alias that matches `Promise<infer U>` once stops at `Promise<number>`, which then fails to assign to a `number` — the classic reason to prefer the built-in over a bespoke `UnwrapPromise`.
- Why is `Awaited<T>` safe to apply to a type parameter that might not be a promise at all?Because it is a pass-through for non-thenables: `Awaited<string>` is `string`. That mirrors `await 5` producing `5`. So a helper accepting either a sync or an async callback can declare `Promise<Awaited<T>>` and be correct in both cases, with no overload and no conditional type of your own.
- Does using `Awaited` mean you can skip the `await` keyword in the implementation?No. Types are erased, so `Awaited` changes nothing at runtime — it only describes what awaiting would produce. If you drop the keyword, you still hold a promise object, and the checker will report that `Promise<User>` is not assignable to `User`. The type-level utility and the runtime keyword are separate obligations.
saying these in an interview costs you the question
- Says an async function's return type is the resolved value
- Thinks Awaited unwraps only one level
- Believes Awaited on a non-promise gives never
- Claims Awaited inserts an await at runtime
- Assumes it only works on the built-in Promise class