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
- You write
ReturnType<typeof fn>orParameters<typeof fn>. typeof fnresolves tofn‘s full call signature as a type, e.g.(a: number, b: number) => number.- 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. - While matching, the compiler binds the
infervariable 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. - 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. Atype User = ReturnType<typeof createUser>statement produces zero bytes of output JS — it exists only fortscand 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
typeofwhenever the source is a declared function or variable holding a function; skip it when you already have a function type (aninterface,typealias, or inline signature). - Reach for
Parameters<T>andReturnType<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 aPromise<...>, not the resolved value — wrap it inAwaited<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 aliasCalculateTotalArgsusingParameters<T>, and write a second functionlogTotalthat accepts a...args: CalculateTotalArgsrest parameter, callscalculateTotal, and logs the result. - Write a function
fetchProduct(id: number)that returns an object literal withid,name, andpricefields. Derive aProducttype from it withReturnType<T>, then write a functionprintProduct(product: Product): voidthat logs"name - $price". - Write a generic function
withRetry<F extends (...args: any[]) => any>(fn: F, ...args: Parameters<F>): ReturnType<F>that simply callsfn(...args)and returns its result (no real retry logic needed). Verify withtscthat the inferred return type of a call towithRetrymatches 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
inferkeyword, constrained toT extends (...args: any) => any. - Use the
typeoftype 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
ReturnTypeon an async function gives you aPromise<...>, not the resolved value.
