The infer Keyword in TypeScript
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> // neverRead 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> // numberNote 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>>> // stringEach 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
: neverTwo 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 | numberIn 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 }> // nevernever 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:
- Awaited — unwrap a promise, then make it recursive
- Parameters — one
inferin a rest position - First of Array — tuple pattern matching, the easy end
- Get Return Type — rebuild
ReturnTypefrom scratch - Parse URL Params —
inferplus template literals plus recursion
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.
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