Alright, let’s get our hands dirty with the real magic: using infer to pull types apart and see what’s inside. This is where conditional types stop being a neat parlor trick and start being the backbone of genuinely powerful and flexible type logic. We’re going to focus on the two most common and useful applications: inferring what a function returns and what it takes as arguments.

Think of infer like a pattern-matching assignment within the extends clause of a conditional type. You’re saying, “Hey TypeScript, if this type matches this general shape, grab the type of this specific part and assign it to my new type variable so I can use it later.” It’s like telling a friend, “If you see a box of donuts, grab me a chocolate éclair and call me.” You’ve defined the condition (a box of donuts) and what to extract (the éclair).

Unpacking a Function’s Return Type

You will, constantly, find yourself in a situation where you have some function type F and you need to know what it returns. Manually copying and pasting its return type is for chumps and is incredibly brittle. Instead, we ask the type system to tell us.

type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

// Let's use it on a built-in function
const parser = (input: string) => parseInt(input, 10);
type WhatIParsed = GetReturnType<typeof parser>; // number

// And on a more complex one
async function fetchUser(id: string) {
  return { id, name: "Alice" };
}
type UserPromise = GetReturnType<typeof fetchUser>; // Promise<{ id: string; name: string; }>

Here’s how it works: We check if T is a subtype of a function (that takes any arguments). If it is, we use infer R to capture whatever type is in the position of the return type and expose it as R. If T isn’t a function, we bail out to never.

This is so common and useful that TypeScript provides a built-in utility type for it: ReturnType<T>. The above example is literally how it’s implemented. Use ReturnType in your real code; build your own GetReturnType to understand how it works.

Grabbing Function Parameters

While grabbing the return type is straightforward, grabbing parameters is where the designers made a… interesting choice. You might think, “I’ll just infer the whole arguments tuple,” and you’d be right. But there’s a twist.

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

function createUser(name: string, age: number, isAdmin: boolean = false) {
  // ... implementation
}
type CreateUserParams = GetParameters<typeof createUser>; // [name: string, age: number, isAdmin?: boolean]

Notice that? The optional isAdmin parameter is correctly inferred as an optional item within the tuple, isAdmin?: boolean. This is fantastic. The built-in equivalent here is Parameters<T>.

But here’s the rub, the classic TypeScript “gotcha”: overloaded functions. If your function is overloaded, Parameters<T> and ReturnType<T> will only give you the types from the last overload declared. This is a deliberate but frustrating design limitation because the type system can’t know which overload you intended to call. It’s a stark reminder that the type system is a model of your code, not the code itself.

// An overloaded function
function badDesign(value: string): number;
function badDesign(value: number): string;
function badDesign(value: string | number): number | string {
  return typeof value === 'string' ? value.length : value.toString();
}

// TypeScript will only look at the last implementation signature
type BadReturn = ReturnType<typeof badDesign>; // string | number
type BadParams = Parameters<typeof badDesign>; // [value: string | number]

The inference doesn’t magically give you a union of all possibilities; it gives you the types from the implementation, which is often much wider and less useful than any specific overload. It’s the type system’s way of throwing its hands up and saying, “You figure it out.”

The Power of infer in Generic Contexts

The real superpower of these techniques isn’t just inspecting known functions; it’s using them inside your own generics to create sophisticated, type-safe utilities.

Imagine you’re writing a wrapper that times how long a function takes to execute. You want to preserve the original function’s signature perfectly—its parameters and its return type (wrapped in a promise if it’s async, which you don’t know ahead of time!).

function withTiming<Fn extends (...args: any[]) => any>(func: Fn): Fn {
  return async (...args: Parameters<Fn>): Promise<ReturnType<Fn>> => {
    const start = Date.now();
    try {
      // @ts-ignore: We're ignoring the complex typing of `await` on a possibly sync value
      const result = await func(...args); // Works for both sync and async functions
      return result;
    } finally {
      console.log(`Function took ${Date.now() - start}ms`);
    }
  } as Fn; // The 'as Fn' cast is needed because we've changed the return type to a Promise
}

const timedFunction = withTiming(fetchUser);
// timedFunction now has the same parameter types as fetchUser (id: string)
// and its return type is now Promise<Promise<User>>... oh no.

Wait, we created a problem. If fetchUser returns Promise<User>, our wrapper returns Promise<Promise<User>>, which is wrong. We need to unwrap a potential promise. This leads us to the concept of “awaited” types, another perfect job for conditional types and infer. But that’s a topic for another section. For now, just appreciate how Parameters and ReturnType allow us to build a function that is generic over any input function’s shape. That’s incredibly powerful.

The infer keyword is your precise tool for surgical type manipulation. Use it to ask the type system questions about other types, and you’ll unlock a level of type safety that feels less like paperwork and more like a superpower.