Alright, let’s get our hands dirty with infer. This is where TypeScript’s type system stops just being a fancy dress-up for your objects and starts feeling like actual programming. It’s the magic trick that lets you reach into a generic type, pull out a piece you care about, and use it elsewhere. It’s the main reason we can build types that feel intelligent and adaptive, rather than just static.

Think of it like a regex capture group, but for types. You’re pattern matching on a type structure and saying, “Hey TypeScript, whatever you find here, in this specific spot, I want to name it and use it.”

The only place you can use the infer keyword is within the extends clause of a conditional type. It’s not a standalone operator; it’s part of the conditional type’s interrogation process.

How infer Works: The Basic Anatomy

Let’s break down the syntax. It always follows this pattern:

type GetSomePart<T> = T extends SomeComplexType<infer U> ? U : never;

You’re telling TypeScript: “Check if T matches the shape of SomeComplexType<something>. If it does, grab that something, call it U, and give it back to me. If it doesn’t, give me never (the type equivalent of going away empty-handed).”

Let’s make this concrete. The classic “Hello, World!” of infer is extracting the type inside a Promise.

type Awaited<T> = T extends Promise<infer U> ? U : T;

// Let's test it
type PromiseOfString = Promise<string>;
type Unwrapped = Awaited<PromiseOfString>; // type Unwrapped = string

// It even handles non-promises gracefully, because of the false branch
type NotAPromise = number;
type Unwrapped2 = Awaited<NotAPromise>; // type Unwrapped2 = number

See what happened? We defined a type Awaited (which, incidentally, is now a built-in utility type as of TypeScript 4.5) that peels away the Promise wrapper and gives us what’s inside. This is profoundly useful for functions that work with async values.

Beyond Promises: Inferring Function Types

Where infer truly flexes its muscles is with function types. You can extract the return type, the parameter types, or even this type. Let’s start with the return type. This is so useful it’s also a built-in type (ReturnType), but we’re going to build it ourselves to understand it.

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

function getString() {
  return "hello";
}

type FnReturn = MyReturnType<typeof getString>; // type FnReturn = string

The logic is identical to the Promise example: “If T is a function, whatever type it returns (=> infer R), give me that R.” The constraint T extends (...args: any) => any is just there to make it clear we expect a function, but the conditional type does the real work.

We can do the same for parameters. The built-in Parameters<T> type gives you a tuple of all function parameters.

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

function logThings(id: number, name: string) {
  console.log(id, name);
}

type LogParams = MyParameters<typeof logThings>; // type LogParams = [id: number, name: string]

This is incredibly powerful for meta-programming. You can write types that create new functions based on the signatures of old ones, which is the foundation of many advanced type-safe decorator and composition patterns.

The One Big Gotcha: It’s a Matcher, Not a Looper

Here’s the most common mistake I see. You can’t use infer to just iterate over arbitrary elements. It only works through pattern matching. For example, you might try to infer the type of an array element like this:

// This works for any array-like type
type GetElementType<T> = T extends (infer U)[] ? U : never;
type Test = GetElementType<string[]>; // string

// But this does NOT work to get the type of the SECOND element.
// You can't just infer in a specific position like this.
type GetSecondElement<T> = T extends [any, infer U, ...any[]] ? U : never;
type Test2 = GetSecondElement<[boolean, string, number]>; // string

The first example works because (infer U)[] is a pattern: “an array of some type U”. The second example works because [any, infer U, ...any[]] is also a pattern: “a tuple where the first element is anything, the second is what we want (U), and the rest can be anything”. This is a crucial distinction. You’re not indexing into the type; you’re providing a template for the entire type structure and asking what fits in a specific slot.

Real-World Power: Inferring from Generics

Let’s say a library gives you a generic type ApiResponse<T> for all its API calls. You have a function that handles the response, but you need to know what T was for a particular response object. infer to the rescue.

interface ApiResponse<TData> {
  data: TData;
  status: number;
  isError: boolean;
}

// A type to extract the TData from any ApiResponse
type ResponseData<T> = T extends ApiResponse<infer U> ? U : never;

// Some function from the library returns this
const userResponse: ApiResponse<{ id: string; name: string }> = ...;

// Now we can get the user type easily
type UserData = ResponseData<typeof userResponse>; // { id: string; name: string }

This is how you start building types that are deeply integrated with the other code in your ecosystem, reducing duplication and keeping everything in sync. It’s not just academic; it’s genuinely useful. infer is the key that unlocks this door. Use it wisely.