skip to content

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?

level: middleimportance: must knowfreq 58%

answer

  1. async means the signature returns a promise
  2. one more step after the return type
  3. unwrapping recurses, not once
  4. non-promise types pass through
  5. unions unwrap member by member

basics

~10 s

An 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 s

An `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 lines
typescript
async 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

for a junior

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.

for a middle

Explain the three behaviours — recursive unwrapping, pass-through for non-thenables, and distribution over unions — and why they make a hand-written unwrapper unnecessary.

for a senior

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.

for a principal

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

context