TypeScript ReturnType and Parameters

ReturnType<T> and Parameters<T> are built-in TypeScript utility types that let you pull the return type or the argument types straight out of an existing function’s signature, instead of writing them out by hand a second time. They matter because functions change: if you manually copy a return type into another interface, that copy silently goes stale the moment the function’s implementation changes. These utilities keep every dependent type derived from a single source of truth — the function itself.

Overview / How it works

ReturnType<T> and Parameters<T> are both examples of TypeScript’s conditional types with inference. They aren’t magic compiler features; you could write equivalent versions yourself. The standard library defines them roughly like this:

type ReturnType<T extends (...args: any) => any> =
  T extends (...args: any) => infer R ? R : any;

type Parameters<T extends (...args: any) => any> =
  T extends (...args: infer P) => any ? P : never;

Both take a generic parameter T that is constrained to any function type ((...args: any) => any). Inside, a conditional type checks "does T match the shape of a function?" — which it always does, because of the constraint — and while checking that shape, the infer keyword captures a piece of the signature into a new type variable (R for the return type, P for the parameter list). Parameters<T> specifically infers a tuple type, so it preserves the number, order, names (in tooling), and optionality of each parameter — not just a loose array.

Because TypeScript’s type system is structural, these utilities work on any value whose type is shaped like a function — function declarations, function expressions, arrow functions, methods, and even function types written directly. The one catch: a function declaration or expression is a runtime value, not a type. To feed it into a type-level utility, you must convert it to a type first using the typeof type operator (not the runtime typeof operator, though it looks identical). That’s why you’ll almost always see ReturnType<typeof myFunction> rather than ReturnType<myFunction>.

One important edge case: if the function has overloads, ReturnType and Parameters only see the last (implementation-visible) overload signature that TypeScript’s inference resolves to — not a union of every overload. For most everyday functions this doesn’t matter, but it’s worth knowing before you rely on it for a heavily overloaded API.

Syntax

type R = ReturnType<typeof someFunction>;
type P = Parameters<typeof someFunction>;
Piece Meaning
typeof someFunction Converts the runtime function value into its type, so it can be passed to a type-level utility.
ReturnType<...> Produces the type of whatever the function returns.
Parameters<...> Produces a tuple type matching the function’s parameter list, in order.
T extends (...args: any) => any The generic constraint both utilities share — T must be some kind of callable.

You can also apply both utilities directly to a named function type (an interface, type alias, or inline function signature) without typeof, since that’s already a type rather than a value.

Examples

1. Deriving a data shape from a factory function

A common pattern is writing a function that builds an object, then wanting a type for "the thing this function produces" elsewhere in the codebase — for example in a function signature that consumes it.

function createUser(name: string, age: number) {
  return {
    id: Math.floor(Math.random() * 1000),
    name,
    age,
    createdAt: new Date(),
  };
}

type User = ReturnType<typeof createUser>;

const printUser = (user: User): void => {
  console.log(`${user.name} (${user.age}) - id: ${user.id}`);
};

const sample: User = {
  id: 42,
  name: "Ada Lovelace",
  age: 28,
  createdAt: new Date(),
};

printUser(sample);

Output:

Ada Lovelace (28) - id: 42

User is inferred as { id: number; name: string; age: number; createdAt: Date } without ever typing that object shape out by hand. If a field is later added to createUser‘s return object, User updates automatically everywhere it’s used.

2. Reusing an argument list with Parameters

Parameters<T> is especially useful when you want to write a wrapper function that must accept exactly the same arguments as another function.

function formatCurrency(
  amount: number,
  currencyCode: string,
  locale: string = "en-US"
): string {
  return new Intl.NumberFormat(locale, {
    style: "currency",
    currency: currencyCode,
  }).format(amount);
}

type FormatCurrencyArgs = Parameters<typeof formatCurrency>;

function logFormatted(...args: FormatCurrencyArgs): void {
  console.log(formatCurrency(...args));
}

logFormatted(1234.5, "USD");
logFormatted(999, "EUR", "de-DE");

Output:

$1,234.50
999,00 €

FormatCurrencyArgs becomes the tuple [amount: number, currencyCode: string, locale?: string]. Because it’s a real tuple, the rest parameter ...args: FormatCurrencyArgs in logFormatted gets exactly the same arity, order, and optionality checks as calling formatCurrency directly — try passing a number where the currency code goes and tsc will reject it.

3. Combining both in a generic higher-order function

Where ReturnType and Parameters really shine is in generic helpers that wrap any function — loggers, memoizers, retry wrappers, and similar decorators.

function add(a: number, b: number): number {
  return a + b;
}

function logCall<F extends (...args: any[]) => any>(
  fn: F,
  ...args: Parameters<F>
): ReturnType<F> {
  console.log(`Calling ${fn.name} with args: ${JSON.stringify(args)}`);
  const result = fn(...args);
  console.log(`Result: ${JSON.stringify(result)}`);
  return result;
}

const sum = logCall(add, 3, 4);
console.log(`sum = ${sum}`);

Output:

Calling add with args: [3,4]
Result: 7
sum = 7

TypeScript infers F from the fn argument you pass in, then uses that same F to type-check the rest of the call (args must match add‘s parameters) and to type the return value (sum is inferred as number, not any). This is the pattern most utility libraries use internally to stay generic while remaining fully type-safe.

How it works step by step / Under the hood

  1. You write ReturnType<typeof fn> or Parameters<typeof fn>.
  2. typeof fn resolves to fn‘s full call signature as a type, e.g. (a: number, b: number) => number.
  3. The utility type’s conditional check (T extends (...args: infer P) => any ? P : never, and similarly for the return type) always matches, because every function type matches the shape (...args: any) => any.
  4. While matching, the compiler binds the infer variable to the exact piece of the signature it lines up with — the parameter tuple or the return type — and that becomes the result of the conditional type.
  5. This all happens purely during type checking. Types are erased at compile time: the emitted JavaScript contains no trace of ReturnType, Parameters, typeof (the type operator), or any type alias you declared with them. A type User = ReturnType<typeof createUser> statement produces zero bytes of output JS — it exists only for tsc and your editor’s IntelliSense.

This is why these utilities are "free" — they add compile-time safety and DRY-ness with no runtime cost whatsoever.

Common Mistakes

Mistake 1: Forgetting typeof on a function value

function getConfig() {
  return { debug: true, env: "production" };
}

type Config = ReturnType<getConfig>;

This fails to compile with an error like: "’getConfig’ refers to a value, but is being used as a type here. Did you mean ‘typeof getConfig’?"getConfig is a runtime value (a function), and ReturnType expects a type argument. The fix is to convert the value to its type with the typeof type operator:

function getConfig() {
  return { debug: true, env: "production" };
}

type Config = ReturnType<typeof getConfig>;

const prodConfig: Config = { debug: false, env: "production" };
console.log(prodConfig);

Output:

{ debug: false, env: 'production' }

Mistake 2: Applying Parameters to something that isn’t a function type

type NotAFunction = string;

type Args = Parameters<NotAFunction>;

tsc reports: "Type ‘string’ does not satisfy the constraint ‘(…args: any) => any’." Parameters<T> is generically constrained to callables, so passing a non-function type like string violates that constraint outright. The fix is to give it an actual function type to work with:

type Logger = (message: string, level?: "info" | "warn" | "error") => void;

type LoggerArgs = Parameters<Logger>;

const callLogger = (...args: LoggerArgs): void => {
  const [message, level = "info"] = args;
  console.log(`[${level.toUpperCase()}] ${message}`);
};

callLogger("Server started");
callLogger("Disk almost full", "warn");

Output:

[INFO] Server started
[WARN] Disk almost full

Note that Logger here is already a function type, so no typeof is needed — typeof is only for converting runtime values into types.

Best Practices

  • Use typeof whenever the source is a declared function or variable holding a function; skip it when you already have a function type (an interface, type alias, or inline signature).
  • Reach for Parameters<T> and ReturnType<T> in generic wrapper, decorator, or higher-order functions (loggers, retry helpers, memoizers) so they automatically stay in sync with whatever function is passed in.
  • For async functions, remember ReturnType<typeof myAsyncFn> gives you a Promise<...>, not the resolved value — wrap it in Awaited<ReturnType<typeof myAsyncFn>> to get the awaited type instead.
  • Be cautious applying these to overloaded functions; they only capture the last matching overload signature, which can be surprising for complex overload sets.
  • Don’t over-derive: if a type is part of your public API contract (e.g. exported from a library), it’s often clearer and more stable to declare it explicitly rather than deriving it, since derived types shift silently whenever the source function changes.
  • Prefer deriving types from a single implementation function over hand-duplicating a parameter or return type in two places — the whole point of these utilities is eliminating that duplication.

Practice Exercises

  • Write a function calculateTotal(price: number, taxRate: number, discount?: number): number. Then derive a type alias CalculateTotalArgs using Parameters<T>, and write a second function logTotal that accepts a ...args: CalculateTotalArgs rest parameter, calls calculateTotal, and logs the result.
  • Write a function fetchProduct(id: number) that returns an object literal with id, name, and price fields. Derive a Product type from it with ReturnType<T>, then write a function printProduct(product: Product): void that logs "name - $price".
  • Write a generic function withRetry<F extends (...args: any[]) => any>(fn: F, ...args: Parameters<F>): ReturnType<F> that simply calls fn(...args) and returns its result (no real retry logic needed). Verify with tsc that the inferred return type of a call to withRetry matches the wrapped function’s return type exactly.

Summary

  • ReturnType<T> extracts a function’s return type; Parameters<T> extracts its parameter list as a tuple type.
  • Both are built from conditional types using the infer keyword, constrained to T extends (...args: any) => any.
  • Use the typeof type operator to convert a function value (declaration, expression, arrow function) into a type before passing it in — function types themselves don’t need it.
  • They’re especially powerful in generic wrapper/decorator functions, keeping argument and return types automatically in sync with the wrapped function.
  • All of this is compile-time only; the compiled JavaScript has no trace of these types — they add zero runtime cost.
  • Watch out for overloaded functions (only the last overload is captured) and remember ReturnType on an async function gives you a Promise<...>, not the resolved value.