Back

Understanding infer in TypeScript

Understanding infer in TypeScript

infer declares a type variable inside the extends clause of a conditional type, capturing a matched sub-type so it can be used in the true branch — it has no meaning outside that position and is a compile error anywhere else. If you’ve opened a library’s .d.ts file or a colleague’s PR and hit something like T extends (...args: any[]) => infer R ? R : any, that line is using infer to pull a piece out of a type the way destructuring pulls a value out of an object.

Key Takeaways

  • infer is only legal inside the extends clause of a conditional type, and the variable it introduces is only in scope in the true branch — referencing it in the false branch is a compiler error.
  • infer was introduced in TypeScript 2.8; template literal infer arrived in 4.1, Awaited<T> in 4.5, and infer X extends Constraint in 4.7.
  • When the type parameter is a naked type variable and you pass a union, the conditional distributes over each member, so infer resolves once per member and the results collect into a union — a common silent-widening bug.
  • Since TypeScript 4.5, Awaited<T> is the built-in for unwrapping Promise chains; there is no reason to re-implement UnwrapPromise manually.
  • Multiple candidates for the same infer variable produce a union in covariant positions and an intersection in contravariant positions.

Conditional types: the substrate infer lives in

A conditional type selects one of two types based on whether one type is assignable to another, using the form T extends X ? A : B. It works like a ternary, but at the type level: if T is assignable to X, the type resolves to A, otherwise to B. The TypeScript handbook on conditional types is the primary reference.

type IsString<T> = T extends string ? true : false;

type A = IsString<"hello">; // true
type B = IsString<42>;      // false

The behavior that trips people up is distributivity. When the checked type is a naked type parameter — T directly, not wrapped — and you pass a union, TypeScript distributes the conditional over each union member separately and collects the results into a new union. Wrapping the parameter in a tuple ([T] extends [X]) turns off distribution and checks the union as a single unit. This distinction is documented under distributive conditional types.

type Distributed<T> = T extends string ? T[] : never;
type  R1  =  Distributed<string | number>; // string[] (number member resolves to never and is eliminated)

type NonDistributed<T> = [T] extends [string] ? T[] : never;
type R2 = NonDistributed<string | number>; // never — checked as one unit

Keep this rule in your head: it is the root of the most common infer surprise, covered in the gotchas section below.

Anatomy of an infer pattern

A type variable introduced with infer is only in scope in the true branch of the conditional type; referencing it in the false branch is a TypeScript compiler error. infer introduces the variable inside an extends pattern and binds it to whatever matches that position when the conditional is evaluated. This makes infer behave a little like destructuring — but for types: you write a pattern that mirrors the shape you expect, name the parts you want to extract, and TypeScript fills them in.

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

type A = ElementType<string[]>; // string
type B = ElementType<number>;   // number

The pattern (infer U)[] says “an array of some element type — call that element type U.” When T is string[], U binds to string. When the match fails, the false branch runs, and U is no longer available there:

type Bad<T> = T extends (infer U)[] ? U[] : U;
//                                          ^ Error: Cannot find name 'U'.

A single pattern can contain multiple infer variables, each binding to its own position:

type SplitFn<T> = T extends (arg: infer A) => infer R ? [A, R] : never;

type S = SplitFn<(x: number) => string>; // [number, string]

Here infer A captures the parameter type and infer R captures the return type in one pass. This is exactly the technique the built-in utility types use.

infer in the standard library: the canonical extractors

The TypeScript standard library implements its extractor utilities with infer. Each follows the same shape: match a pattern, bind the interesting position, return it in the true branch. The definitions below are the ones shipped in lib/es5.d.ts.

Function return type — ReturnType

ReturnType<T> extracts the return type of a function type by matching the function shape and binding the return position to infer R. The standard-library definition constrains T to be callable.

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

type A = ReturnType<() => string>;        // string
type B = ReturnType<() => { id: number }>; // { id: number }

Note the generic constraint T extends (...args: any) => any — it is part of the actual standard-library definition, not optional decoration.

Takeaway: ReturnType is the cleanest worked example of infer — pattern-match the function, capture the return slot.

Function parameters as a tuple — Parameters

Parameters<T> extracts a function’s parameter list as a tuple by binding the rest-parameter position to infer P.

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

type Args = Parameters<(a: string, b: number) => void>; // [a: string, b: number]

Takeaway: Because parameters are modeled as a tuple type, infer P over ...args gives you the whole list, preserving labels and optionality.

Promise resolution — use Awaited<T>, not a hand-rolled unwrapper

Since TypeScript 4.5, the built-in Awaited<T> recursively unwraps Promise chains and handles PromiseLike — there is no reason to re-implement UnwrapPromise manually in modern TypeScript. Many older tutorials define their own T extends Promise<infer U> ? U : T, but that single-level version doesn’t unwrap nested promises or model .then-able objects. Awaited does both, as described in the TypeScript 4.5 release notes.

type A = Awaited<Promise<string>>;            // string
type B = Awaited<Promise<Promise<number>>>;   // number — recursive
type C = Awaited<boolean | Promise<number>>;  // number | boolean

Takeaway: Reach for Awaited<T> directly. Write your own infer-based promise unwrapper only as a learning exercise.

Array element type

Extracting an array’s element type is the smallest useful infer pattern: match (infer U)[] and return U.

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

type A = ElementOf<number[]>;          // number
type B = ElementOf<readonly string[]>; // string

Takeaway: Include readonly in the pattern so the utility works for both mutable and readonly arrays.

Tuple head, tail, and rest

Tuples support positional infer patterns with rest elements, letting you pull the head, the tail, or the last element. The patterns mirror JavaScript array destructuring.

type Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never;
type Tail<T extends any[]> = T extends [any, ...infer Rest] ? Rest : never;
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;

type H = Head<[1, 2, 3]>; // 1
type R = Tail<[1, 2, 3]>; // [2, 3]
type L = Last<[1, 2, 3]>; // 3

Takeaway: [infer Head, ...infer Rest] and [...any[], infer Last] are the building blocks for recursive tuple manipulation — the same patterns power type-level routers and form-state libraries.

Recursive inference — Flatten

infer combined with recursion lets a type peel back arbitrarily deep structures. Flatten recurses through nested arrays until it reaches a non-array type.

type Flatten<T> = T extends Array<infer U> ? Flatten<U> : T;

type A = Flatten<number[][][]>; // number
type B = Flatten<string>;       // string

Each pass binds U to the element type and feeds it back into Flatten. When T is no longer an array, the false branch returns it unchanged.

Takeaway: Recursive infer is how you collapse nested containers; the same shape unwraps deeply nested Promise or object types.

Template literal inference — parsing strings at the type level

Template literal types support infer inside their substitution positions. The ExtractId<T> example below matches the string pattern and binds the variable segment to Id, enabling typed route-parameter extraction without a runtime parser — a pattern available since TypeScript 4.1.

type ExtractId<T> = T extends `/users/${infer Id}` ? Id : never;

type A = ExtractId<"/users/42">;   // "42"
type B = ExtractId<"/posts/42">;   // never

You can chain infer across multiple segments to parse a whole path into its parts:

type RouteParams<T> =
  T extends `${infer _Start}/:${infer Param}/${infer Rest}`
    ? Param | RouteParams<`/${Rest}`>
    : T extends `${infer _Start}/:${infer Param}`
      ? Param
      : never;

type P = RouteParams<"/users/:userId/posts/:postId">; // "userId" | "postId"

This is the type-level equivalent of a path parser, and it is the foundation of typed routing in frameworks and libraries that derive param objects from route strings.

Takeaway: Template literal infer turns string-shaped types into structured data with zero runtime cost — useful for route params, CSS-property unions, and parsing branded string formats.

Constrained inference: infer X extends Constraint (TS 4.7)

TypeScript 4.7 added infer X extends Constraint, letting a conditional type constrain an inferred variable at the point of matching — combining inference and constraint checking in a single step. This is documented in the TypeScript 4.7 release notes. Before 4.7, you had to infer first and then add a second nested conditional to narrow the result.

// Before 4.7: infer, then check in a second conditional
type FirstStringOld<T> =
  T extends [infer H, ...any[]]
    ? H extends string ? H : never
    : never;

// 4.7+: constrain at the point of inference
type FirstString<T> =
  T extends [infer H extends string, ...any[]] ? H : never;

type A = FirstString<["hi", 1, 2]>; // "hi"
type B = FirstString<[1, 2, 3]>;    // never

TypeScript 4.8 later improved constrained inference for primitive types such as number, bigint, and boolean, allowing the compiler to preserve more precise literal types when matching template-string patterns. This builds on the constrained infer syntax introduced in TypeScript 4.7, but was delivered as a separate enhancement.

Takeaway: Use infer X extends C when you want both the captured type and a guarantee about its shape in one expression — it replaces the older infer-then-nest pattern.

Three gotchas that silently break inference

The three gotchas that silently break infer are distributivity over unions widening results, variance flipping multiple infer candidates between union and intersection, and overly specific patterns falling through to the false branch. infer failures are usually silent — the type still compiles, it’s just wider or narrower than you intended.

1. Distributivity silently widens results

When the type parameter T is a naked type variable and you pass a union, TypeScript distributes the conditional over each member separately — meaning infer R resolves once per union member and the results are collected into a union, which can silently widen the inferred type beyond what you intended.

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

// Looks like it returns one type; actually distributes
type R = Unwrap<Promise<string> | Promise<number>>; // string | number

If you wanted to reject mixed unions rather than collapse them, wrap the parameter to disable distribution:

type UnwrapStrict<T> = [T] extends [Promise<infer U>] ? U : T;

Takeaway: A naked T plus a union means per-member evaluation. Wrap in [T] when you need the union treated as a single unit.

2. Covariant vs. contravariant position changes the result

Multiple candidates for the same infer variable produce a union in covariant positions (e.g., return types) and an intersection in contravariant positions (e.g., function parameter types). For overloaded functions, inference uses the last signature. This behavior is described in the TypeScript 2.8 release notes.

// Covariant (return position): union
type Co<T> = T extends { a: () => infer U; b: () => infer U } ? U : never;
type C = Co<{ a: () => string; b: () => number }>; // string | number

// Contravariant (parameter position): intersection
type Contra<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void } ? U : never;
type D = Contra<{ a: (x: string) => void; b: (x: number) => void }>; // string & number

Takeaway: Reusing one infer name across positions is intentional and powerful, but whether you get a union or an intersection depends on the variance of the positions. Don’t assume union.

3. Patterns that can’t bind fall through to the false branch

infer only binds when the candidate type structurally matches the pattern. If the shape doesn’t match, the conditional takes the false branch and the inferred variable never resolves — so a too-specific pattern silently returns your fallback type instead of the value you expected.

type FirstArg<T> = T extends (a: infer A, b: infer B) => any ? A : never;

type X = FirstArg<string>; // never — string does not match a function type

In this example, FirstArg expects a function type. Passing string doesn’t match that pattern, so the conditional resolves to the false branch and returns never.

Takeaway: Make patterns as loose as the match allows. Use ...args: infer P or [infer H, ...any[]] rather than fixed-arity shapes unless you specifically want to reject other arities.

Where you actually meet infer in the wild

In application code you rarely write infer yourself — you encounter it in library type definitions like React’s ComponentProps, Express’s RequestHandler, Redux Toolkit’s PayloadAction, and RTK Query endpoint types, and use the resulting utility types. Recognizing the pattern T extends Wrapper<infer X> ? X : Fallback lets you read those definitions instead of treating them as black boxes.

React ComponentProps. The @types/react typings define ComponentProps with infer to pull a component’s props out of its type, branching between host elements and component constructors. Reading it, you can see the same T extends ... infer P ... ? P : ... shape from ReturnType, applied to React’s element types.

Express request handlers. Express’s @types/express models RequestHandler as a generic with parameters for route params, response body, request body, and query. Libraries that derive typed route handlers from a path string combine these generics with template literal infer to extract :param segments — the RouteParams pattern shown earlier is the core of that machinery.

Redux Toolkit PayloadAction. PayloadAction from Redux Toolkit is a useful type to extract from with infer, even though its own definition relies on generics and conditional types rather than infer internally — a reminder that infer is how you read a library’s types, not necessarily how the library is written. You’ll commonly write type Payload<A> = A extends PayloadAction<infer P> ? P : never to recover a reducer’s payload type from its action.

import type { PayloadAction } from "@reduxjs/toolkit";

type PayloadOf<A> = A extends PayloadAction<infer P> ? P : never;

type P = PayloadOf<PayloadAction<{ id: string }>>; // { id: string }

RTK Query endpoints. RTK Query (part of Redux Toolkit) exposes generated endpoint types where the query and result types are recoverable with infer-based utilities. Extracting an endpoint’s result type with a conditional that binds the result position is a routine task once you recognize the pattern.

Wrapping up

infer is one feature with one rule: it names a position inside a conditional-type pattern and binds whatever matches there, usable only in the true branch. Once you see T extends Pattern<infer X> ? X : Fallback as type-level destructuring, every built-in utility and library typing reads as a variation on that single move. The next time you hit infer in a .d.ts file, trace the pattern: find the position the variable sits in, check whether the parameter is naked or wrapped, and confirm you’re reading the true branch. Start by replacing any hand-rolled UnwrapPromise in your codebase with Awaited<T>, and adopt infer X extends Constraint where you’re currently nesting a second conditional to narrow a result.

FAQs

infer extracts a type out of an existing type by pattern-matching inside a conditional, while a mapped type transforms each property of a type into a new shape. Use infer when you need to read a sub-type from a structure, such as a function's return type or an array's element type. Use a mapped type when you need to iterate over keys and produce a new object type. They solve opposite problems: infer reads, mapped types rewrite.

No. A type variable introduced with infer is only in scope in the true branch of the conditional type; referencing it in the false branch produces a TypeScript compiler error such as 'Cannot find name'. The variable only exists once a match succeeds, so the false branch has no value to bind. If you need a value in both outcomes, infer in the true branch and supply an unrelated fallback type in the false branch.

Because of distributivity. When the type parameter is a naked type variable and you pass a union, TypeScript distributes the conditional over each member separately, so infer resolves once per union member and the results collect into a new union. This can silently widen the result beyond what you intended. To disable distribution and check the union as one unit, wrap the parameter in a tuple, as in [T] extends [Promise<infer U>] ? U : T.

No, not in modern TypeScript. Since TypeScript 4.5, the built-in Awaited<T> recursively unwraps Promise chains and handles PromiseLike thenable objects. A hand-rolled T extends Promise<infer U> ? U : T only unwraps one level and ignores then-able objects, so it breaks on nested promises. Reach for Awaited<T> directly in application code, and write your own infer-based unwrapper only as a learning exercise.

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay