infer Keyword — Extracting types from patterns#
What it is#
infer is a single-purpose TypeScript keyword that declares a fresh type variable inside the extends clause of a conditional type. When the conditional matches, the compiler fills infer X with whatever sub-type satisfied that position; when it doesn’t match, the false branch runs and X never exists. That sounds abstract, but it’s the trick behind every utility type that “reads inside” another type — ReturnType<T> reads the return position, Parameters<T> reads the parameter tuple, Awaited<T> reads the resolved type of a promise, and so on.
The keyword is only legal inside a conditional type’s extends position. You cannot use infer as a generic parameter or inside an interface; the compiler will reject it anywhere else. Think of it as “let me name this slot so I can refer to it on the other side of the ternary.”
Install#
Nothing to install. infer shipped in TypeScript 2.8 (March 2018). All examples below assume TS 5.x for the extends infer X chained syntax and recursive conditional types.
npx tsc --version
Output:
Version 5.5.4
Syntax#
type Extracted<T> = T extends SomePattern<infer X> ? X : Fallback;
Output: (none — pure type syntax)
The T extends ... ? ... : ... ternary is the conditional type. The infer X introduces a brand-new type variable scoped to that conditional. If T matches the pattern, X is bound to whatever filled that slot; otherwise the false branch runs.
Essential rules#
| Rule | Detail |
|---|---|
Only inside extends | infer X must appear in the extends clause of a conditional type |
| Fresh per use | Every infer X introduces a new variable, even with the same name |
| Multiple inferences | A single conditional can have many infer slots |
| Distributive over unions | Generic T extends ... distributes by default; wrap [T] extends [...] to opt out |
| Recursive | A conditional may reference itself in the true branch (recursive conditional types, TS 4.1+) |
infer X extends Y | TS 4.7+ — constrain the inferred type to a bound |
Fallback to never | When the conditional doesn’t match, use never so unions filter cleanly |
The simplest example — array element#
The canonical infer example: extract the element type of an array. The pattern (infer E)[] matches any array, and E is bound to the element type.
type ElementOf<T> = T extends (infer E)[] ? E : never;
type A = ElementOf<string[]>; // string
type B = ElementOf<number[][]>; // number[]
type C = ElementOf<Array<{ id: 1 }>>; // { id: 1 }
type D = ElementOf<string>; // never — not an array
Output: (none — all four are compile-time type aliases)
If you’ve ever wished for a way to say “give me whatever’s inside an array,” that’s infer. The same trick works for any generic shape — Promise<infer R>, Map<infer K, infer V>, Record<string, infer V>, etc.
Built-in utilities that use infer#
Almost every built-in utility type with a name like “extract” or “unwrap” is implemented with a single conditional + infer. Their definitions are short enough to memorise.
// lib.es5.d.ts (paraphrased)
type ReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : any;
type Parameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never;
type ConstructorParameters<T extends abstract new (...args: any) => any> =
T extends abstract new (...args: infer P) => any ? P : never;
type InstanceType<T extends abstract new (...args: any) => any> =
T extends abstract new (...args: any) => infer R ? R : any;
type Awaited<T> =
T extends null | undefined ? T :
T extends object & { then(onfulfilled: infer F, ...args: infer _): any } ?
F extends (value: infer V, ...args: infer _) => any ? Awaited<V> : never :
T;
type ThisParameterType<T> =
T extends (this: infer U, ...args: never) => any ? U : unknown;
Output: (none — these are declarations only)
Notice the recursion in Awaited — calling Awaited<V> inside the true branch is how nested promises get peeled.
Multiple infer slots in one conditional#
A single conditional may declare more than one infer. The compiler binds each independently.
// Extract key and value types from a Map<K, V>
type MapTypes<T> = T extends Map<infer K, infer V>
? { key: K; value: V }
: never;
type R = MapTypes<Map<string, number>>;
// { key: string; value: number }
// Pull the first and second positions of a tuple
type FirstTwo<T> = T extends [infer A, infer B, ...unknown[]]
? [A, B]
: never;
type S = FirstTwo<[1, "two", true, 4]>;
// [1, "two"]
Output: (none)
Inferring function pieces — Parameters, ReturnType, FirstArg#
Functions are a tuple of parameters plus a return type, and infer can read any slice of them.
type Fn = (x: number, y: string, opts: { force?: boolean }) => Promise<boolean>;
// Whole parameter tuple
type Params = Parameters<Fn>;
// [x: number, y: string, opts: { force?: boolean }]
// Just the return type
type Ret = ReturnType<Fn>;
// Promise<boolean>
// First parameter — pattern matches a head + variadic rest
type First<F> = F extends (first: infer X, ...rest: any[]) => any ? X : never;
type X = First<Fn>; // number
// Last parameter — works because tuples support variadic patterns
type Last<F> = F extends (...args: [...infer _, infer L]) => any ? L : never;
type L = Last<Fn>; // { force?: boolean }
// Parameters minus the first
type Rest<F> = F extends (first: any, ...rest: infer R) => any ? R : never;
type RestArgs = Rest<Fn>;
// [y: string, opts: { force?: boolean }]
Output: (none)
Tuple manipulation — Head, Tail, Length#
Tuple types are spreadable in conditional patterns, which makes Lisp-style tuple operations natural to express with infer.
type Head<T extends unknown[]> = T extends [infer H, ...unknown[]] ? H : never;
type Tail<T extends unknown[]> = T extends [unknown, ...infer R] ? R : [];
type Last<T extends unknown[]> = T extends [...unknown[], infer L] ? L : never;
type Init<T extends unknown[]> = T extends [...infer I, unknown] ? I : [];
type T = [1, 2, 3, 4];
type H1 = Head<T>; // 1
type T1 = Tail<T>; // [2, 3, 4]
type L1 = Last<T>; // 4
type I1 = Init<T>; // [1, 2, 3]
// Length — uses the literal "length" property
type Length<T extends unknown[]> = T["length"];
type N = Length<T>; // 4
Output: (none)
infer can pattern-match around a ... spread, which is why these definitions are concise.
Recursive conditional types#
A conditional may reference itself in either branch. Combined with infer, this lets you express “keep extracting until you can’t anymore.”
// Recursive Awaited — peel nested promises
type DeepAwaited<T> = T extends Promise<infer U> ? DeepAwaited<U> : T;
type A = DeepAwaited<Promise<Promise<Promise<string>>>>; // string
type B = DeepAwaited<number>; // number
// Flatten an arbitrarily nested array
type DeepFlatten<T> = T extends Array<infer Inner>
? DeepFlatten<Inner>
: T;
type C = DeepFlatten<string[][][]>; // string
type D = DeepFlatten<number>; // number
// Reverse a tuple
type Reverse<T extends unknown[]> = T extends [infer H, ...infer R]
? [...Reverse<R>, H]
: [];
type E = Reverse<[1, 2, 3]>; // [3, 2, 1]
Output: (none)
TS limits recursion depth — usually 50 or 100 instantiations — but that’s plenty for typical use.
infer with constraints (TS 4.7+)#
You can constrain an inferred type with infer X extends Y. This avoids needing a second extends check inside the true branch and lets you keep narrow literal types.
// Before 4.7 — clumsy
type FirstNum<T> = T extends [infer A, ...unknown[]]
? A extends number ? A : never
: never;
// 4.7+ — clean
type FirstNum47<T> = T extends [infer A extends number, ...unknown[]] ? A : never;
type N1 = FirstNum47<[3, "two", true]>; // 3
type N2 = FirstNum47<["x", 1]>; // never
// Parsing literal numbers from template strings — keeps the literal narrow
type ToNumber<S extends string> = S extends `${infer N extends number}` ? N : never;
type Year = ToNumber<"2026">; // 2026 (literal, not just `number`)
Output: (none)
infer in template literal types#
Template literal types support infer exactly the way generic type wrappers do — name any string slot to read it out.
// Parse a route like "/users/:id"
type RouteParam<S extends string> =
S extends `${string}:${infer P}/${infer Rest}`
? P | RouteParam<`/${Rest}`>
: S extends `${string}:${infer P}`
? P
: never;
type P = RouteParam<"/users/:userId/posts/:postId">;
// "userId" | "postId"
// Split a string on a delimiter
type Split<S extends string, D extends string> =
S extends `${infer Head}${D}${infer Tail}`
? [Head, ...Split<Tail, D>]
: [S];
type Parts = Split<"2026-05-25", "-">;
// ["2026", "05", "25"]
// Replace
type Replace<S extends string, From extends string, To extends string> =
S extends `${infer L}${From}${infer R}` ? `${L}${To}${R}` : S;
type R1 = Replace<"hello world", "world", "TS">;
// "hello TS"
Output: (none)
Distributive vs non-distributive#
When the conditional’s left-hand side is a bare generic parameter and T is a union, the conditional distributes — once over each union member, then results unioned back. To opt out, wrap both sides in a one-tuple [T] extends [U].
// Distributive — runs once per union member
type Boxed<T> = T extends string ? { kind: "s"; value: T } :
T extends number ? { kind: "n"; value: T } :
never;
type B = Boxed<string | number>;
// { kind: "s"; value: string } | { kind: "n"; value: number }
// Non-distributive
type IsExactlyString<T> = [T] extends [string] ? true : false;
type Y1 = IsExactlyString<"hello">; // true — literal extends string
type Y2 = IsExactlyString<string | number>; // false — not all members are string
Output: (none)
infer works with both forms; the distribution rule applies to the conditional itself, not to the infer slots.
Custom DeepReadonly with infer#
Combining recursion, mapped types, and infer over the array branch yields a real DeepReadonly that handles plain objects, arrays, tuples, and maps without freezing functions or primitives.
type DeepReadonly<T> =
T extends (...args: any[]) => any ? T :
T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepReadonly<U>> :
T extends Map<infer K, infer V> ? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>> :
T extends Set<infer U> ? ReadonlySet<DeepReadonly<U>> :
T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } :
T;
interface AppConfig {
server: { host: string; ports: number[] };
db: Map<string, { url: string }>;
}
type Frozen = DeepReadonly<AppConfig>;
// {
// readonly server: { readonly host: string; readonly ports: ReadonlyArray<number> };
// readonly db: ReadonlyMap<string, { readonly url: string }>;
// }
Output: (none)
Promisify with infer#
Wrap every property type of an object in a Promise, leaving function-typed properties alone except to promisify their return. A real “promisify” with infer on both the function signature and its return.
type Promisify<T> = {
[K in keyof T]: T[K] extends (...args: infer A) => infer R
? (...args: A) => Promise<Awaited<R>>
: Promise<T[K]>;
};
interface SyncApi {
count: number;
add(a: number, b: number): number;
who(): string;
}
type AsyncApi = Promisify<SyncApi>;
// {
// count: Promise<number>;
// add: (a: number, b: number) => Promise<number>;
// who: () => Promise<string>;
// }
Output: (none)
Common pitfalls#
- Using
inferoutsideextends—inferis only legal in a conditional type’s extends clause. Anywhere else (a generic parameter, an interface, a mapped type body) is a syntax error. - Forgetting to default to
never— A non-matching false branch should usually benever, notunknownor the input. Returning the input pollutes unions:T extends Promise<infer U> ? U : TmakesAwaited<number>resolve tonumber, which is what you want — butT extends Promise<infer U> ? U : neverwould erase non-promises entirely. Choose based on intent. - Distribution surprises —
T extends Foo<infer X> ? X : neverdistributes over a unionT, producing a union ofXs. Wrap with[T] extends [Foo<infer X>]to stop distribution if you wanted the union as a whole. anypoisons inference — Inferring insideanyreturnsany. Constrain withunknownor a precise pattern.- Excessive depth errors — Recursive conditional types hit the compiler’s recursion limit (currently around 50–100). Reformulate or use an accumulator tuple to lower the depth.
infer X extends Yrequires TS 4.7+ — The constrained form is a relatively recent addition. Confirm yourtsconfigtarget/liband TS version before relying on it.- Optional vs
undefinedin tuple inference —[infer A, ...infer R]requires at least one element. For possibly-empty tuples, pattern-match both[]and[H, ...T]cases. - Inferring
this—thisis a parameter slot for the type checker:T extends (this: infer U, ...args: any) => any ? U : never. Forgetting thethis:form gives you the first real argument instead. - Inferring from index signatures —
T extends Record<string, infer V>matches any object, because every object structurally satisfies that pattern. To target intentional records, constrainTfirst. - Naming
infer Xafter a generic parameter doesn’t reuse it —type Foo<X> = T extends infer X ? ... : ...shadows the outerXwith a fresh, unrelated type variable. Rename the inner one if you actually want both visible.
Real-world recipes#
Recipe 1 — Typing the result of a Zod-style parser#
You want to know what parse(schema) returns without re-declaring the shape. Use infer over the schema’s parse method.
interface Parser<T> {
parse(input: unknown): T;
}
type ParsedOf<S> = S extends Parser<infer T> ? T : never;
const userSchema = {
parse(input: unknown): { id: number; name: string } {
return input as { id: number; name: string };
},
};
type User = ParsedOf<typeof userSchema>;
// { id: number; name: string }
const u: User = userSchema.parse({ id: 1, name: "Alice" });
console.log(u.name);
Output:
Alice
Recipe 2 — Extract the args of a generic API client#
When a method’s signature is hidden behind a generic, infer gives you each piece for use elsewhere (e.g. building a wrapper that retries or logs).
class ApiClient {
async get<T>(path: string): Promise<T> {
return fetch(path).then((r) => r.json()) as Promise<T>;
}
async post<T>(path: string, body: unknown): Promise<T> {
return fetch(path, { method: "POST", body: JSON.stringify(body) })
.then((r) => r.json()) as Promise<T>;
}
}
type MethodOf<C, K extends keyof C> = C[K];
type ResultOf<M> = M extends (...args: any[]) => Promise<infer R> ? R : never;
type GetResult = ResultOf<MethodOf<ApiClient, "get">>; // unknown (generic erased)
type PostResult = ResultOf<MethodOf<ApiClient, "post">>; // unknown
// Use directly with the call site to keep the T narrow
async function withRetry<R>(fn: () => Promise<R>, n = 3): Promise<R> {
let lastErr: unknown;
for (let i = 0; i < n; i++) {
try { return await fn(); } catch (e) { lastErr = e; }
}
throw lastErr;
}
const api = new ApiClient();
const user = await withRetry(() => api.get<{ name: string }>("/me"));
console.log(user.name);
Output:
Alice
Recipe 3 — Parse route params at the type level#
Type-check a router’s handler against the :param segments in its path, with no runtime overhead. infer walks the template literal.
type RouteParams<S extends string> =
S extends `${string}:${infer Param}/${infer Rest}`
? { [K in Param | keyof RouteParams<`/${Rest}`>]: string }
: S extends `${string}:${infer Param}`
? { [K in Param]: string }
: Record<string, never>;
type Handler<S extends string> = (req: { params: RouteParams<S> }) => unknown;
const userPostHandler: Handler<"/users/:userId/posts/:postId"> = (req) => {
return `${req.params.userId}/${req.params.postId}`;
};
console.log(userPostHandler({ params: { userId: "alice", postId: "42" } }));
Output:
alice/42
Recipe 4 — Build a type-safe currying helper#
Currying takes a multi-arg function and turns it into a chain of single-arg functions. With infer you can express the chain at the type level.
type Curry<F> =
F extends (a: infer A, ...rest: infer R) => infer Ret
? R extends []
? (a: A) => Ret
: (a: A) => Curry<(...rest: R) => Ret>
: never;
function curry<F extends (...args: any[]) => any>(fn: F): Curry<F> {
const c = (acc: unknown[]) =>
(x: unknown) => {
const all = [...acc, x];
return all.length >= fn.length ? fn(...all) : c(all);
};
return c([]) as Curry<F>;
}
const add3 = (a: number, b: number, c: number): number => a + b + c;
const curried = curry(add3);
console.log(curried(1)(2)(3));
Output:
6
Recipe 5 — Unwrap nested promises in a generic safeRun#
safeRun returns [err, null] | [null, value] for any function, sync or async. infer recovers the success type even through nested promises.
type Result<T> = [Error, null] | [null, T];
type Resolved<F> = F extends (...args: any[]) => infer R
? Awaited<R>
: never;
async function safeRun<F extends (...args: any[]) => any>(
fn: F,
...args: Parameters<F>
): Promise<Result<Resolved<F>>> {
try {
const value = await fn(...args);
return [null, value as Resolved<F>];
} catch (e) {
return [e instanceof Error ? e : new Error(String(e)), null];
}
}
async function fetchScore(): Promise<Promise<number>> {
return Promise.resolve(42);
}
const [err, score] = await safeRun(fetchScore);
// ^ score: number | null
if (!err) console.log("score:", score);
Output:
score: 42
Recipe 6 — Discriminator type from a class hierarchy#
Walk a class’s static members with infer to pull out the discriminant string from a kind literal field.
abstract class Shape {
abstract readonly kind: string;
}
class Circle extends Shape {
readonly kind = "circle" as const;
constructor(public radius: number) { super(); }
}
class Square extends Shape {
readonly kind = "square" as const;
constructor(public side: number) { super(); }
}
type KindOf<C> = C extends new (...args: any[]) => { kind: infer K } ? K : never;
type Kinds = KindOf<typeof Circle | typeof Square>;
// "circle" | "square"
function describe(s: Circle | Square): string {
return s.kind === "circle" ? `circle r=${s.radius}` : `square s=${s.side}`;
}
console.log(describe(new Circle(5)));
console.log(describe(new Square(3)));
Output:
circle r=5
square s=3
Recipe 7 — Pulling a query’s argument types from an SQL-ish template#
Use infer over template literal types to detect ? placeholders in a SQL string and produce a tuple of unknown (or any other type) the same length.
type Placeholders<S extends string, Acc extends unknown[] = []> =
S extends `${string}?${infer Rest}`
? Placeholders<Rest, [...Acc, unknown]>
: Acc;
function query<S extends string>(sql: S, ...args: Placeholders<S>): void {
console.log("running:", sql, "with", args);
}
query("SELECT * FROM users WHERE id = ? AND name = ?", 1, "Alice");
// query("SELECT * FROM users WHERE id = ?", 1, "Alice"); // Error — too many args
Output:
running: SELECT * FROM users WHERE id = ? AND name = ? with [ 1, 'Alice' ]
Recipe 8 — Combining infer X extends number to validate config keys#
Parse a config key like "timeout-30s" and statically extract the numeric portion as a literal type.
type ExtractSeconds<S extends string> =
S extends `timeout-${infer N extends number}s` ? N : never;
type T1 = ExtractSeconds<"timeout-30s">; // 30
type T2 = ExtractSeconds<"timeout-5s">; // 5
type T3 = ExtractSeconds<"hold-on">; // never
function setTimeoutKey<K extends string>(key: K, _value: ExtractSeconds<K>): void {
console.log("set", key);
}
setTimeoutKey("timeout-30s", 30);
// setTimeoutKey("timeout-30s", 31); // Error — value must literally be 30
// setTimeoutKey("hold-on", 1); // Error — never
Output:
set timeout-30s