skip to content

ReturnType, Parameters & Awaited

Deriving types from functions and classes you already wrote, so the type follows the implementation instead of drifting from it. Interviewers use these to check that you understand typeof-on-a-value plus infer working underneath.

part ofTypeScriptoverview, primer and where to startread it →
on this pageshow

questions

6

In TypeScript, a function `createUser` returns an object literal whose shape you never declared. How do you get a named type for that return value without writing the shape twice, and why does `ReturnType<createUser>` fail to compile?

level: juniorimportance: must knowfreq 72%

answer

  1. values and types are separate namespaces
  2. a function name is only a value
  3. bridge with a type query
  4. the utility needs a function type, not a value
  5. typeof f, then unwrap the return

basics

~10 s

ReturnType<typeof createUser> gives the type. In a type position, typeof turns the value createUser into its function type, which ReturnType then unwraps. ReturnType<createUser> fails because createUser is a value, not a type.

solid answer

~40 s

Write `type User = ReturnType<typeof createUser>`. TypeScript keeps two separate namespaces: a function declaration puts `createUser` into the *value* namespace only, so using the bare name in a type position gives "'createUser' refers to a value, but is being used as a type here. Did you mean 'typeof createUser'?". The `typeof` type query bridges the two — it takes a value and yields its type, here the function's signature — and `ReturnType<T>` is a built-in utility constrained to `(...args: any) => any` that pulls out the return type. The payoff is a single source of truth: change what `createUser` returns and every consumer of `User` updates automatically. All of this is erased at compile time; the emitted JavaScript is unchanged.

go deeper

for a junior

Be able to write ReturnType<typeof myFn> from memory and say plainly that the bare function name is a value, so typeof is required to reach its type.

for a middle

Explain the two namespaces, that ReturnType is an ordinary conditional type constrained to (...args: any) => any, and why the inferred return type is widened rather than literal.

for a senior

Show where deriving is worth it — store state, factory results, keeping consumers in step with the implementation — and where a hand-written declared type at a module boundary is the safer contract.

for a principal

Own the policy: which types in the codebase are derived from implementations and which are declared and enforced onto them, and what that choice does to refactor blast radius and error-message quality.

## Two namespaces in one file TypeScript maintains two separate namespaces over the same source. The **value namespace** holds things that exist when the program runs: `const`, `let`, `function`, and the constructor half of a `class`. The **type namespace** holds things that exist only during checking and are erased before emit: `interface`, `type` aliases, type parameters, and the instance half of a `class`. A function declaration puts its name into the value namespace *only*. So when you write `ReturnType<createUser>`, you are asking the checker to look up a **type** called `createUser`, and there isn't one. The compiler is explicit about the fix: ```ts function createUser() { return { id: 1, name: "Ada", role: "admin" }; } type User = ReturnType<createUser>; // Error: 'createUser' refers to a value, but is being used as a type here. // Did you mean 'typeof createUser'? ``` ## What `typeof` means in a type position In a type position, `typeof x` is a **type query**: it takes a value and produces that value's static type. It is a different construct from the runtime `typeof` operator that evaluates to the strings `"string"`, `"object"` and friends — same keyword, different namespace, and the type query never survives compilation. `typeof createUser` is the type `() => { id: number; name: string; role: string }`. The operand of a type query is restricted. It must be an identifier or a dotted path to a value — `typeof store`, `typeof store.getState`, or `typeof import("./config")` — not an arbitrary expression. `typeof createUser()` is not legal; you cannot call something in a type position, because nothing in the type layer runs. ## What `ReturnType` actually is `ReturnType` is not compiler magic; it is a one-line conditional type shipped in TypeScript's own library: ```ts type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any; ``` The constraint `T extends (...args: any) => any` is exactly why the `typeof` is mandatory: the argument must already be a **function type**. Hand it a value and the lookup fails before the constraint is even considered. So the composite `ReturnType<typeof createUser>` reads right-to-left: take the value `createUser`, ask for its type (a function signature), then extract the return type from that signature. ## Why derive instead of declare The alternative is to declare the shape by hand and annotate the function with it. That is the right call at a published API boundary. Inside a module, though, hand-written duplicates drift: someone adds a field to the returned object, forgets the interface, and the two disagree until a bug surfaces. Deriving makes the implementation the single source of truth, which is why the idiom is everywhere in real codebases — most famously for store state: ```ts type RootState = ReturnType<typeof store.getState>; ``` ## The widening gotcha A derived type reflects what the checker **inferred**, not what you see in the source. Object literals returned from a function get their property types widened to the general primitive: ```ts function createUser() { return { id: 1, role: "admin" }; } type User = ReturnType<typeof createUser>; // { id: number; role: string } — not { id: 1; role: "admin" } ``` If you wanted the literal types, the fix belongs at the function — return the object with a `const` assertion — not at the `ReturnType` call site. Candidates who expect `role` to be `"admin"` here have not internalised that inference, not the utility, decides the answer. ## Nothing happens at runtime A final point interviewers listen for: `ReturnType<typeof createUser>` does not call `createUser`, does not inspect it, and costs nothing. Types are erased — the utility resolves entirely during checking, and the emitted JavaScript is byte-identical whether you wrote the type by hand or derived it.

  • If `createUser` returns `{ role: "admin" }`, why is the derived type `{ role: string }`, and how would you keep the literal?
    Object literal properties widen during inference, so the checker records `string`. `ReturnType` faithfully reports what was inferred. To keep the literal, fix it at the source — return the object with an `as const` assertion, or annotate the function's return type explicitly. Nothing you do at the `ReturnType` call site can un-widen a type that was already widened.
  • Can you write `ReturnType<typeof createUser()>` to skip a step?
    No. A type query's operand must be an identifier or a dotted path to a value, never an expression — calls are not permitted, because nothing in the type layer executes. `typeof createUser` already yields the full signature, and `ReturnType` extracts the return type from it, so there is nothing to skip.
  • Does `typeof` in a type position emit any JavaScript?
    None. It is a compile-time type query that is erased with the rest of the type annotations. It shares a keyword with the runtime `typeof` operator, but they live in different namespaces: one asks the checker for a value's static type, the other evaluates at runtime to a string. Deriving a type has zero runtime cost.

saying these in an interview costs you the question

  • Writes ReturnType<createUser> without the typeof
  • Thinks the type query runs or calls the function
  • Says typeof in a type position emits a runtime check
  • Expects literal types where inference widened them
  • Cannot say why a function name is not a type

context

open as a page

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%

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.

open as a page

In TypeScript, given `class HttpClient { constructor(baseUrl: string, timeoutMs: number) {} }`, how do you derive a tuple of its constructor arguments and its instance type, and why does `InstanceType<HttpClient>` fail to compile?

level: middleimportance: should knowfreq 42%

basics

~10 s

Use ConstructorParameters<typeof HttpClient> for the argument tuple and InstanceType<typeof HttpClient> for the instance. InstanceType<HttpClient> fails because the bare class name already means the instance type; the constructor side is reached only through typeof HttpClient.

open as a page

In TypeScript, you are wrapping an existing `sendEmail(to: string, subject: string, body: string): boolean` with a logging version. How do you type the wrapper so its parameters stay in sync with `sendEmail`, and what exactly is the type `Parameters<typeof sendEmail>`?

level: middleimportance: should knowfreq 55%

basics

~10 s

Declare the wrapper as (...args: Parameters<typeof sendEmail>) and spread args into the call. Parameters<typeof sendEmail> is the labelled tuple [to: string, subject: string, body: string], so adding or reordering parameters updates the wrapper automatically.

open as a page

In TypeScript, what do `ReturnType` and `Parameters` give you when applied to an overloaded function and to a generic function, and why does that break a wrapper built on them?

level: seniorimportance: should knowfreq 34%

basics

~20 s

For an overloaded function both utilities see only the last overload signature, silently discarding the others. For a generic function the type parameters are already instantiated — unconstrained ones become unknown — so the caller's argument type never reaches the return type.

open as a page

In TypeScript, when should a shared type be declared explicitly and enforced onto the implementation, rather than derived from it with `ReturnType<typeof ...>`?

level: principalimportance: nice to knowfreq 26%

basics

~20 s

Declare the type wherever it is a contract others depend on: a package's public API, a cross-team boundary, a persisted or wire shape. Derive inside a module where the implementation is genuinely the source of truth, such as store state or internal factory results.

open as a page