skip to content

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%

answer

  1. extraction resolves to one signature
  2. overloads: last one wins
  3. the others vanish with no warning
  4. type parameters are instantiated first
  5. unconstrained becomes unknown

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.

solid answer

~40 s

Both utilities extract from a *single* signature, and that is where they fall down. Against an overloaded function the checker matches the **last** overload: with `ov(x: string): number` then `ov(x: number): boolean`, `ReturnType<typeof ov>` is `boolean` and `Parameters<typeof ov>` is `[x: number]` — the other overload silently vanishes, so a wrapper typed this way rejects half the legitimate calls. Against a generic function the type parameters are instantiated before extraction: for `identity<T>(x: T): T`, `ReturnType<typeof identity>` is `unknown`, and with `T extends string` it collapses to `string`. Either way the link between argument and result is gone. The fix is not another utility — make the wrapper generic itself, or re-declare the overloads on it. This is TypeScript 5.x and 6.x behaviour.

code

typescript · 20 lines
typescript
declare function ov(x: string): number;
declare function ov(x: number): boolean;

type R = ReturnType<typeof ov>; // boolean  — only the LAST overload
type P = Parameters<typeof ov>; // [x: number]

declare function identity<T>(x: T): T;
type G = ReturnType<typeof identity>; // unknown — T was instantiated away

declare function firstChar<T extends string>(x: T): T;
type C = ReturnType<typeof firstChar>; // string — collapsed to the constraint

// The wrapper that keeps the relationship is generic in its own right.
function loggedIdentity<T>(x: T): T {
  console.log(x);
  return identity(x);
}

const kept: "a" = loggedIdentity("a" as const);
console.log(kept);

go deeper

for a junior

Know that these utilities read a function's signature, and that a function with overloads or type parameters is not a simple single signature to copy.

for a middle

Be able to state that the last overload wins and that a generic's type parameters are instantiated — unconstrained to unknown, constrained to the constraint — before extraction happens.

for a senior

Recognise the failure in review: a wrapper that compiles while its call sites break, or a derived alias hovering as unknown, and know the fix is a polymorphic wrapper rather than a cleverer type.

for a principal

Set the boundary for the team: derivation forwards concrete signatures, polymorphic contracts are declared. Cleverer type-level workarounds cost check time and readability for little gain.

## Both utilities read one signature `ReturnType<T>` and `Parameters<T>` are conditional types that match `T` against a single function shape. A function *type* in TypeScript can carry several call signatures, but the extraction still resolves to one. That is the root of both failures below. ## Overloads: the last signature wins ```ts declare function ov(x: string): number; declare function ov(x: number): boolean; type R = ReturnType<typeof ov>; // boolean type P = Parameters<typeof ov>; // [x: number] ``` Inference against a type with multiple call signatures picks the **last** one. Nothing warns you; the earlier overloads are simply not represented. This is quietly destructive in a wrapper: ```ts function logged(...args: Parameters<typeof ov>): ReturnType<typeof ov> { return ov(...args); } logged("hello"); // Error — the wrapper only accepts a number now ``` The wrapper compiles, looks correct in review, and rejects a call the original function accepted. The remedy is to declare the overloads on the wrapper too, or to type the wrapper against the underlying implementation shape rather than derive it. There is no built-in utility that returns a union of all overload signatures. ## Generics: the parameters are already gone ```ts declare function identity<T>(x: T): T; type R1 = ReturnType<typeof identity>; // unknown declare function firstChar<T extends string>(x: T): T; type R2 = ReturnType<typeof firstChar>; // string ``` When the checker extracts from a generic signature, it instantiates the type parameters first — an unconstrained parameter becomes `unknown`, a constrained one collapses to its constraint. The extraction then reports that instantiated result. This is not a bug; there is genuinely no single return type for a generic function, because the answer depends on the call. But the consequence is that deriving destroys exactly the property that made the function generic: the *relationship* between what goes in and what comes out. ```ts // Wrong: every call now returns unknown. function loggedIdentity( ...args: Parameters<typeof identity> ): ReturnType<typeof identity> { return identity(...args); } // Right: the wrapper carries its own type parameter. function loggedIdentity2<T>(x: T): T { console.log(x); return identity(x); } ``` The generic wrapper is more typing and it is the only version that preserves the caller's type. ## How to spot it before it ships The symptom in review is a wrapper that type-checks while its call sites start failing, or a derived alias that hovers as `unknown` where you expected a concrete shape. Two habits catch it: - Before deriving from a function, look at whether the declaration has more than one signature or any type parameters. If it does, deriving is the wrong tool. - Assert the derived type rather than trusting the hover. A line such as `const _check: ReturnType<typeof ov> = someNumber;` fails loudly when the extraction picked a signature you did not expect. ## A related trap: the implementation signature An overloaded function's *implementation* signature — the wide one written last, often with `any` or union parameters — is not visible to callers at all. Candidates sometimes assume `Parameters` will surface it since it is physically last in the file. It does not: only the declared overloads participate, and the implementation signature is deliberately excluded from the function's external type. So the choice is between the declared overloads, and the last of those is the one you get. ## The judgment to demonstrate The senior answer is not "here is the workaround" but "here is when derivation is the wrong shape of tool". `ReturnType` and `Parameters` are for transparently forwarding a **single concrete signature**. The moment the function's contract is polymorphic — overloads, generics, conditional return types — the wrapper needs its own polymorphic declaration, because a derived alias can only ever record one instantiation of a contract that has many. Reaching for a cleverer type-level workaround here usually produces something slower to check and harder for the next reader than simply writing the wrapper's signature out.

  • How would you correctly wrap an overloaded function without losing its earlier signatures?
    Declare the overloads on the wrapper as well, then implement it with a wide implementation signature that forwards. There is no built-in utility that returns all call signatures of an overloaded type, so the signatures have to be restated. If the overload set is large, that duplication is itself a signal the underlying API might be better modelled as a generic or a discriminated parameter.
  • Why does `ReturnType<typeof identity>` give `unknown` for `identity<T>(x: T): T` rather than an error?
    There is no single return type for a generic function — it depends on the call — so the checker instantiates the type parameter before extracting. Unconstrained, that instantiation is `unknown`; with `T extends string` it becomes `string`. The result is well-defined and useless for a wrapper, which is worse than an error because it compiles.
  • Is the implementation signature of an overloaded function ever what `Parameters` returns?
    No. The implementation signature is not part of the function's external type — callers cannot see it, and neither can the extraction. Only the declared overloads participate, and inference selects the last of those. Assuming the wide implementation signature is visible because it appears last in the file is a common misreading.

saying these in an interview costs you the question

  • Expects a union of all overload signatures
  • Thinks the first overload is selected
  • Believes the implementation signature is what gets extracted
  • Assumes the caller's type argument survives into the derived type
  • Says a generic function's derived return type is an error, not unknown

context