The infer Keyword in TypeScript

October 4, 202610 min read
Requirements:
Generics| Unions

A variable for the type system

You have seen it in a library's type definitions: T extends (infer U)[] ? U : never. It looks like a magic word. It is not. infer declares a variable for the type system, and every infer example you will ever read does the same thing — match a shape, capture one piece of it, hand that piece back.

The rule that makes infer click: it only exists inside the extends clause of a conditional type. You are not asking "is this type an array?" — you are asking "is this type an array of something, and if so, what was the something?"

What infer actually does

A conditional type is a type-level if. A extends B ? X : Y checks whether A is assignable to B and picks a branch.

infer turns the right-hand side of that check into a pattern. Instead of naming a concrete type, you leave a hole and give it a name. If TypeScript can make the check succeed by filling that hole, it binds the name to whatever it filled it with, and the true branch can use it.

That is the whole feature. Everything below is the same idea with different shapes.

Your first infer example: unwrap an array

type ElementOf<Source> = Source extends (infer Item)[] ? Item : never
 
type Names = ElementOf<string[]> // string
type Mixed = ElementOf<(number | boolean)[]> // number | boolean
type NotAnArray = ElementOf<number> // never

Read it as a question: is Source an array of something? For string[], TypeScript fills the hole with string to make the check pass, so Item is string. For number there is no way to make it pass, so you fall into the false branch.

The parentheses matter. (infer Item)[] is an array of the inferred type. infer Item[] is a syntax error — the array brackets have to wrap the whole infer expression.

Unwrapping a Promise

Same pattern, different container:

type Unpromise<Source> = Source extends Promise<infer Value> ? Value : Source
 
type UserRecord = { id: number; name: string }
type Resolved = Unpromise<Promise<UserRecord>> // UserRecord
type Untouched = Unpromise<number> // number

Note the false branch. Returning Source instead of never makes this safe to apply to anything — a value that was never a promise comes back unchanged. That single decision is the difference between a type you can compose and one that quietly collapses to never halfway through a chain.

The built-in Awaited<T> does the same job recursively, so it also unwraps a Promise<Promise<string>>. A conditional type is allowed to call itself, which makes the recursive version almost as short as the single-level one:

type DeepAwaited<Source> = Source extends Promise<infer Inner>
  ? DeepAwaited<Inner>
  : Source
 
type DoubleWrapped = DeepAwaited<Promise<Promise<string>>> // string

Each pass peels one layer and feeds the result back in. When the check finally fails, the false branch hands back whatever is left — which is the value you were after all along.

Pulling a function apart

The most common real use is reaching into a function type you did not write:

function loadUser(id: number) {
  return { id, name: 'Ada', active: true }
}
 
type ReturnOf<Fn> = Fn extends (...args: never[]) => infer Result ? Result : never
 
type LoadedUser = ReturnOf<typeof loadUser>
// { id: number; name: string; active: boolean }

You never wrote that object type by hand, and you never have to keep it in sync. Add a field to the return value and LoadedUser follows.

This is not a trick — it is literally how the standard library does it. Here are ReturnType and Parameters from lib.es5.d.ts, renamed so they do not clash:

type MyReturnType<Fn extends (...args: any) => any> = Fn extends (
  ...args: any
) => infer R
  ? R
  : any
 
type MyParameters<Fn extends (...args: any) => any> = Fn extends (
  ...args: infer P
) => any
  ? P
  : never

Two lines of real code each. Utility types stop being black boxes the moment you can read their one-liner.

Two infer sites in one condition

Nothing stops you from leaving several holes:

type SplitSignature<Fn> = Fn extends (...args: infer Args) => infer Out
  ? { args: Args; out: Out }
  : never
 
declare function saveDraft(title: string, wordCount: number): boolean
 
type DraftSignature = SplitSignature<typeof saveDraft>
// { args: [title: string, wordCount: number]; out: boolean }

Args comes back as a labelled tuple, parameter names included. That tuple is what makes wrapper functions — loggers, retries, memoisers — typeable without any: a wrapper declares (...args: Parameters<Fn>) and the call site keeps the argument names and arity of the function it wrapped, hover text included.

Both holes are filled from a single successful match. TypeScript does not run the check twice; it matches the whole signature once and binds every name it found along the way.

The same name twice: union or intersection

Reuse one name in two positions and TypeScript has to reconcile both matches. Which way it goes depends on where the holes sit.

In a normal (covariant) position, you get a union:

type FirstOrSecond<Pair> = Pair extends { a: infer Both; b: infer Both }
  ? Both
  : never
 
type Widened = FirstOrSecond<{ a: string; b: number }> // string | number

In a parameter position, which is contravariant, you get an intersection:

type HandlerPayload<Handlers> = Handlers extends {
  onA: (value: infer Payload) => void
  onB: (value: infer Payload) => void
}
  ? Payload
  : never
 
type Intersected = HandlerPayload<{
  onA: (value: { id: number }) => void
  onB: (value: { name: string }) => void
}>
// { id: number } & { name: string }

If that split feels arbitrary, it is the same rule that governs union types everywhere else: a value coming out can be either, so it widens to a union; a value going in has to satisfy both, so it narrows to an intersection.

Infer inside template literal types

Template literal types and infer together let you parse strings at compile time:

type RouteParam<Path> = Path extends `${string}:${infer Param}/${string}`
  ? Param
  : Path extends `${string}:${infer Param}`
    ? Param
    : never
 
type UserParam = RouteParam<'/users/:userId'> // 'userId'
type NestedParam = RouteParam<'/users/:userId/posts'> // 'userId'

The ordering is deliberate: the greedier pattern with the trailing segment goes first, and the simpler one catches whatever is left. Pattern matching at the type level has the same fall-through logic as a switch.

Constrained inference with infer X extends

Before TypeScript 4.8, a captured template-literal piece was always a string. You had to re-check it with a nested conditional to get anything better. Now you can constrain the hole directly:

type NumericId<Value> = Value extends `${infer Digits extends number}`
  ? Digits
  : never
 
type ParsedId = NumericId<'42'> // 42, a number literal
 
type LooseId<Value> = Value extends `${infer Raw}` ? Raw : never
 
type StillAString = LooseId<'42'> // '42'

infer Digits extends number does two jobs at once: it only matches when the captured text parses as a number, and it hands you the numeric literal rather than the string. Worth reaching for whenever the thing you captured has a narrower type than the position suggests.

Recursion: the tail of a tuple

Combine infer with a rest element and you can walk a tuple one item at a time:

type TailOf<Items> = Items extends [unknown, ...infer Rest] ? Rest : never
 
type Shifted = TailOf<[1, 2, 3]> // [2, 3]

Every recursive type you have admired — a router parser, a deep-readonly, a join of string literals — is this block calling itself until the tuple runs out.

The unknown in the first position is a placeholder for "one item, whatever it is". Swap it for infer Head and you get both halves at once, which is the standard shape for walking a tuple: handle Head, recurse on Rest, stop when the pattern no longer matches an empty list. TypeScript caps that recursion depth, so these types are for fixed-size tuples and short string literals, not for grinding through a thousand-element list.

When you do not need infer

infer is the right tool when the piece you want is buried in a shape the type system has to match. It is the wrong tool when you can just index into the type, and indexed access is both shorter and easier to read:

const statuses = ['draft', 'review', 'published'] as const
 
type Status = (typeof statuses)[number] // 'draft' | 'review' | 'published'
 
type LoadedUserName = ReturnOf<typeof loadUser>['name'] // string

(typeof statuses)[number] gives you the element type of an array without a conditional anywhere in sight, and ['name'] reaches into an object type the same way a property access reaches into a value. The rule of thumb: if you can name the position you want, index it. Reach for infer when the position only exists if the shape matches — inside a function signature, behind a Promise, after a : in a route string.

The same goes for keyof. Capturing a key with infer works, but keyof Source already hands you every key as a union, and it keeps working when the object has forty properties instead of one.

Where infer goes wrong

Outside an extends clause it is a syntax error. There is no type-level let:

type Broken<Source> = infer Item
// ❌ 'infer' declarations are only permitted in the 'extends'
//    clause of a conditional type

A no-match falls to the false branch, and never is a sharp default.

type ValueOfBox<Source> = Source extends { value: infer Held } ? Held : never
 
type HeldNumber = ValueOfBox<{ value: number }> // number
type HeldNothing = ValueOfBox<{ label: string }> // never

never propagates silently — it does not error where it is produced, it errors three types later where something expects a value. If a sensible passthrough exists, return Source the way Unpromise does.

A naked type parameter distributes over unions. ElementOf<string[] | number[]> runs the check once per member and unions the results, which is usually what you want. When it is not, wrap both sides in a tuple — [Source] extends [(infer Only)[]] — to switch distribution off.

infer with no pattern is a useful no-op. Source extends infer Copy always matches, which makes it the standard trick for forcing TypeScript to display a type flattened instead of as an intersection:

type Flatten<Source> = Source extends infer Copy
  ? { [Key in keyof Copy]: Copy[Key] }
  : never
 
type MergedProps = Flatten<{ id: number } & { name: string }>
// { id: number; name: string }

The type is identical either way. The tooltip is a lot friendlier.

Where to practice

infer sticks when you implement the built-ins yourself:

From there, mapped types are the other half of type-level programming, and generics are the foundation both sit on.

The takeaway

infer is a hole in a pattern with a name attached. Match a shape in the extends clause, capture the piece you care about, use it in the true branch. Arrays, promises, function signatures, tuples and string literals are all the same move with different brackets.

Start with the one-liners from the standard library. Once you can write ReturnType yourself, the rest of the type-level ecosystem stops looking like magic and starts looking like code you could have written.

Share this article

Become a TypeScript Pro

Track your progress through 200+ hands-on challenges. Free, sign in with GitHub.

Or start solving right away: explore all TypeScript challenges

Practice with Challenges

Put your the infer keyword in typescript knowledge to the test with these related challenges.

#189Awaited
Easy
#3312Parameters
Easy
#14First of Array
Easy
#2Get Return Type
Medium
#15Last of Array
Medium
#9616Parse URL Params
Medium

Related Concepts

Concepts that build on or relate to the infer keyword in typescript.

TypeScript GenericsTypeScript Utility TypesTemplate Literal TypesTypeScript TuplesUnion Types