TypeScript infer Keyword

The infer keyword lets you reach inside another type and pull a piece of it out, right in the middle of a conditional type expression. Instead of hand-writing a type and hoping it matches some nested shape, you ask the compiler to capture that shape for you and bind it to a brand-new type variable. It’s the exact mechanism that powers many of TypeScript’s built-in utility types, including ReturnType, Parameters, and Awaited. Once you understand infer, you stop copy-pasting structural shapes and start writing type-level functions that derive one type from another automatically.

Overview: How infer Works

infer only exists inside the extends clause of a conditional type — a type of the form T extends U ? X : Y. Normally, U is a fixed type you’re testing T against. When you write infer Name somewhere inside U, you’re telling the compiler: "whatever type shows up in this exact position when T matches the pattern, capture it and call it Name." That captured type then becomes available in the true branch (X) of the conditional.

This works because TypeScript’s type checker already has to perform structural pattern matching to evaluate T extends U. It walks T‘s structure and U‘s structure side by side, position by position. Everywhere U has a concrete type, the checker verifies compatibility. Everywhere U has an infer placeholder, the checker instead records "whatever occupies this slot in T is the answer" and solves for it, the same way type inference solves for a generic parameter from a function argument. If T doesn’t match the pattern shape at all, none of the infer variables get bound, and the conditional falls through to its false branch.

Crucially, this is all compile-time only. There is no runtime concept of infer — conditional types, generics, and inferred type variables are erased entirely when TypeScript compiles to JavaScript. The compiled output contains plain values and plain functions; the "magic" only exists to help the compiler tell you (and your editor) what type something will have.

Syntax

The general shape is: take a conditional type, and inside the pattern you’re checking against, replace a piece you want to capture with infer SomeName.

type UnwrapArray<T> = T extends (infer Item)[] ? Item : T;
  • T — the input type being examined; this is the generic parameter passed in by the caller.
  • extends (infer Item)[] — the pattern T is checked against. Here the pattern is "an array of something." The parentheses around infer Item are required because infer binds loosely; without them the compiler would misparse the expression.
  • infer Item — declares a new type variable, Item, that the compiler solves for using whatever type occupies the array’s element position in T.
  • ? Item : T — if the pattern matched, resolve to Item (the captured element type); otherwise fall back to T unchanged.

infer can appear in many different structural positions, not just arrays. Here are the patterns you’ll see most often:

Pattern What it extracts Used by
T extends (...args: any[]) => infer R ? R : never A function’s return type ReturnType<T>
T extends (infer U)[] ? U : never An array’s element type custom ElementType helpers
T extends Promise<infer U> ? U : T The value a Promise resolves to Awaited<T>
T extends [infer Head, ...infer Rest] ? Head : never The first element of a tuple tuple utilities
T extends { data: infer D } ? D : never A specific property’s value type API response unwrapping

Examples

Example 1: Rebuilding ReturnType from scratch. This shows the classic use case — capturing what a function returns without writing that shape out twice.

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

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

type User = MyReturnType<typeof createUser>;

const user: User = { name: "Ada", age: 30, id: 42 };
console.log(user);
Output:
{ name: 'Ada', age: 30, id: 42 }

TypeScript checks typeof createUser (a function type) against the pattern (...args: any[]) => infer R. The parameter list matches any function via any[], and the return position lines up with infer R, so R is solved as { name: string; age: number; id: number } — the object createUser actually returns. User becomes that object shape, letting user be checked against it without ever writing the interface by hand.

Example 2: Extracting an array’s element type. This is the pattern used to build things like a generic "first element" or "flatten" helper.

type ElementType<T> = T extends (infer U)[] ? U : never;

type StringArray = string[];
type NumberArray = number[];

type StrElem = ElementType<StringArray>;
type NumElem = ElementType<NumberArray>;

const names: StrElem[] = ["Grace", "Ada", "Margaret"];
console.log(names.join(", "));
Output:
Grace, Ada, Margaret

Here T is checked against "an array of something." When T is string[], the something is string, so StrElem resolves to string. Notice the alias itself is generic and reusable — it works identically for NumberArray, producing number, without a separate definition for every element type.

Example 3: Unwrapping a Promise‘s resolved value. This mirrors what the built-in Awaited utility type does, and is common when writing helpers around async APIs.

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

async function getUser(): Promise<{ id: number; name: string }> {
  return { id: 1, name: "Lin" };
}

type UserResult = Awaited2<ReturnType<typeof getUser>>;

function logUser(user: UserResult): void {
  console.log(`${user.id}: ${user.name}`);
}

getUser().then(logUser);
Output:
1: Lin

ReturnType<typeof getUser> is Promise<{ id: number; name: string }>. That gets fed into Awaited2, whose pattern Promise<infer U> matches, binding U to the object type inside the promise. UserResult is therefore the plain object shape, not the promise wrapper — exactly what logUser needs to accept, since .then() hands it the resolved value.

Under the Hood: Step by Step

When the compiler evaluates a conditional type containing infer, it performs roughly these steps:

1. It takes the concrete type substituted for the generic parameter (say, the argument you passed to ElementType<...>).

2. It attempts to structurally match that type against the pattern in the extends clause, treating every infer X as a wildcard slot rather than a fixed type.

3. If the shapes align, it records what filled each wildcard slot; if there are multiple valid candidates (for example, the same inferred name used in more than one covariant position), it combines them into a union. If the same name appears in more than one contravariant position (such as multiple function parameters), it combines them into an intersection instead.

4. If the shapes don’t align at all, no inference happens and the conditional falls through to the false branch, where any infer variables simply don’t exist.

5. The resulting type is substituted into whichever branch was chosen, and that’s the final type — entirely computed before any JavaScript runs. After compilation, the emitted JS has no trace of the type, the conditional, or the inference; it’s just the object, function, or value your code produced.

TypeScript’s variadic tuple support also lets infer capture "the rest" of a parameter list, which is how library utilities extract just the last argument of a function:

type LastArg<T> = T extends (...args: [...infer _, infer Last]) => any ? Last : never;

function greet(greeting: string, name: string, exclamations: number): void {
  console.log(`${greeting}, ${name}${"!".repeat(exclamations)}`);
}

type FinalParam = LastArg<typeof greet>;

const count: FinalParam = 3;
console.log(count);
Output:
3

The pattern [...infer _, infer Last] treats the parameter list as a tuple, captures everything except the final slot into a discarded variable _, and binds the last slot to Last. Even though greet was declared with ordinary named parameters, TypeScript can still match its parameter list against a tuple pattern this way.

Common Mistakes

Mistake 1: Using infer outside a conditional type’s extends clause. infer is only legal as part of the pattern being checked against — it can’t stand on its own as the thing being checked.

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

This fails with: 'infer' declarations are only permitted in the 'extends' clause of a conditional type. The mistake is putting infer U on the left of extends (as the type being tested) instead of inside the pattern on the right. The fix flips it around so T is tested against a pattern containing infer U:

type ExtractElement<T> = T extends (infer U)[] ? U : never;

type Item = ExtractElement<number[]>;
const item: Item = 100;
console.log(item);
Output:
100

Mistake 2: Forgetting that readonly arrays don’t match a plain array pattern. A very common surprise: an infer pattern written for mutable arrays silently produces never for readonly arrays instead of an error at the definition site — the error shows up later, wherever you try to use the result.

type ElementType<T> = T extends (infer U)[] ? U : never;

const readonlyNums: readonly number[] = [1, 2, 3];
type Elem = ElementType<typeof readonlyNums>;

const n: Elem = readonlyNums[0];

This fails with: Type 'number' is not assignable to type 'never'. Because readonly number[] does not match the mutable pattern (infer U)[], Elem silently resolves to never, and any attempt to assign a real value to it errors out. The fix is to make the pattern accept readonly arrays explicitly:

type ElementType<T> = T extends readonly (infer U)[] ? U : never;

const readonlyNums: readonly number[] = [1, 2, 3];
type Elem = ElementType<typeof readonlyNums>;

const n: Elem = readonlyNums[0];
console.log(n);
Output:
1

Best Practices

  • Always wrap the pattern side of an infer in parentheses when it’s part of a larger type expression, such as (infer U)[], to avoid parsing ambiguity.
  • Write a fallback branch (: never or : T) that makes sense for your use case — never to signal "this shouldn’t happen," or the original T to pass values through unchanged when the pattern doesn’t match.
  • When working with arrays or tuples that might be readonly, match against readonly (infer U)[] so both mutable and readonly inputs work.
  • Prefer TypeScript’s built-in utilities (ReturnType, Parameters, Awaited, InstanceType) over reinventing them — reach for custom infer patterns only when you need a shape those utilities don’t cover.
  • Keep each conditional type focused on capturing one thing; if you need several extractions, compose small infer-based aliases rather than one deeply nested conditional.
  • Remember infer only exists at compile time — don’t expect to inspect or branch on an inferred type at runtime; it has already been erased by the time your code executes.

Practice Exercises

Exercise 1: Write a generic type MyParameters<T> that uses infer to extract a function’s parameter list as a tuple, so that MyParameters<(a: string, b: number) => void> evaluates to [string, number].

Exercise 2: Write a generic type ArrayItem<T> that extracts the element type from either a mutable or a readonly array, and resolves to never for any type that isn’t an array.

Exercise 3: Write a recursive generic type DeepUnwrap<T> that fully unwraps nested Promise types — so that DeepUnwrap<Promise<Promise<string>>> resolves all the way down to string, not just one level.

Summary

  • infer declares a new type variable inside the pattern of a conditional type’s extends clause, capturing whatever type occupies that structural position.
  • It powers built-in utilities like ReturnType, Parameters, and Awaited, and lets you write your own precise type-extraction helpers.
  • The compiler solves for infer variables through structural pattern matching — the same mechanism used for ordinary generic inference.
  • Multiple uses of the same inferred name combine as a union in covariant positions and as an intersection in contravariant (parameter) positions.
  • infer is compile-time only; it’s fully erased from the emitted JavaScript.
  • Watch out for readonly arrays and tuples not matching mutable patterns, and remember infer can only appear inside the pattern, never as the type being tested.