skip to content

infer Keyword — Extracting types from patterns

TypeScript's infer keyword declares a fresh type variable inside a conditional type, letting you pull a sub-type out of a matched shape. It powers ReturnType, Parameters, Awaited, and most advanced type extractors.

15 min read 30 snippets deep dive

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#

RuleDetail
Only inside extendsinfer X must appear in the extends clause of a conditional type
Fresh per useEvery infer X introduces a new variable, even with the same name
Multiple inferencesA single conditional can have many infer slots
Distributive over unionsGeneric T extends ... distributes by default; wrap [T] extends [...] to opt out
RecursiveA conditional may reference itself in the true branch (recursive conditional types, TS 4.1+)
infer X extends YTS 4.7+ — constrain the inferred type to a bound
Fallback to neverWhen 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#

  1. Using infer outside extendsinfer is only legal in a conditional type’s extends clause. Anywhere else (a generic parameter, an interface, a mapped type body) is a syntax error.
  2. Forgetting to default to never — A non-matching false branch should usually be never, not unknown or the input. Returning the input pollutes unions: T extends Promise<infer U> ? U : T makes Awaited<number> resolve to number, which is what you want — but T extends Promise<infer U> ? U : never would erase non-promises entirely. Choose based on intent.
  3. Distribution surprisesT extends Foo<infer X> ? X : never distributes over a union T, producing a union of Xs. Wrap with [T] extends [Foo<infer X>] to stop distribution if you wanted the union as a whole.
  4. any poisons inference — Inferring inside any returns any. Constrain with unknown or a precise pattern.
  5. 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.
  6. infer X extends Y requires TS 4.7+ — The constrained form is a relatively recent addition. Confirm your tsconfig target/lib and TS version before relying on it.
  7. Optional vs undefined in tuple inference[infer A, ...infer R] requires at least one element. For possibly-empty tuples, pattern-match both [] and [H, ...T] cases.
  8. Inferring thisthis is a parameter slot for the type checker: T extends (this: infer U, ...args: any) => any ? U : never. Forgetting the this: form gives you the first real argument instead.
  9. Inferring from index signaturesT extends Record<string, infer V> matches any object, because every object structurally satisfies that pattern. To target intentional records, constrain T first.
  10. Naming infer X after a generic parameter doesn’t reuse ittype Foo<X> = T extends infer X ? ... : ... shadows the outer X with 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