DEV Community

Kai Thorne
Kai Thorne

Posted on

TypeScript `infer`: The Keyword That Unlocks Advanced Type Extraction

TypeScript infer: The Keyword That Unlocks Advanced Type Extraction

If you've ever wanted to pluck the return type out of a function signature, grab the element type from an array, or unwrap a Promise — without maintaining a parallel type definition — you're looking for TypeScript's infer keyword.

infer is the difference between writing this:

type UserData = { id: number; name: string; email: string };

async function fetchUser(id: number): Promise<UserData> {
  const res = await fetch(`/users/${id}`);
  return res.json();
}

// Manual duplication — if fetchUser changes, this breaks silently
type FetchedUser = { id: number; name: string; email: string };
Enter fullscreen mode Exit fullscreen mode

And writing this:

type Return<T> = T extends (...args: any[]) => infer R ? R : never;
type Unwrap<T> = T extends Promise<infer V> ? V : never;

// Extracted automatically — always in sync
type FetchedUser = Unwrap<Return<typeof fetchUser>>;
//   ^? { id: number; name: string; email: string }
Enter fullscreen mode Exit fullscreen mode

No duplication. No drift. One source of truth.


What Actually Is infer?

infer is a keyword that only works inside the extends clause of a conditional type. It declares a type variable that TypeScript fills in from context — think of it as pattern matching for types.

type SomeConditional<T> = T extends SomePattern<infer Extracted>
  ? /* use Extracted here */
  : /* fallback */;
Enter fullscreen mode Exit fullscreen mode

The rule: you can only use an inferred type variable inside the true branch (?) of the conditional. TypeScript figures out what Extracted should be by matching T against SomePattern.


Pattern 1: Extract Return Types (The "Hello World")

Every TypeScript developer has seen ReturnType<T>. Here's how it works:

type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function greet(name: string): string {
  return `Hello, ${name}!`;
}

function fetchConfig(): { theme: string; version: number } {
  return { theme: "dark", version: 2 };
}

type GreetResult = MyReturnType<typeof greet>;
//   ^? string

type Config = MyReturnType<typeof fetchConfig>;
//   ^? { theme: string; version: number }
Enter fullscreen mode Exit fullscreen mode

The infer R tells TypeScript: "match the return type of this function signature and assign it to R". If T is a function type, R becomes whatever type it returns. If T isn't a function, the never branch kicks in.

⚠️ Only works with typeof on the function, not a call. Pass the type of the function, not its result:

type Wrong = MyReturnType<fetchConfig>;        // ❌ 'fetchConfig' refers to a value
type Right = MyReturnType<typeof fetchConfig>; // ✅ Works
Enter fullscreen mode Exit fullscreen mode

Pattern 2: Unwrap Arrays

Same pattern, different inference target:

type ArrayElement<T> = T extends Array<infer E> ? E : never;

type A = ArrayElement<string[]>;       // string
type B = ArrayElement<number[]>;       // number
type C = ArrayElement<({ name: string; age: number } | { name: string; admin: boolean })[]>; 
//   ^? { name: string; age: number } | { name: string; admin: boolean }
type D = ArrayElement<number>;         // never (not an array)
Enter fullscreen mode Exit fullscreen mode

This works with ReadonlyArray too:

type Element<T> = T extends readonly (infer E)[] ? E : never;
type E = Element<readonly string[]>; // string
Enter fullscreen mode Exit fullscreen mode

Pattern 3: Unwrap Promises

Probably the most practical pattern in async TypeScript codebases:

type Awaited<T> = T extends Promise<infer V> ? Awaited<V> : T;

// Single level
type Inner = Awaited<Promise<string>>;
//   ^? string

// Recursive unwrapping
type Deep = Awaited<Promise<Promise<Promise<number>>>>;
//   ^? number

// Non-promise passes through
type Plain = Awaited<string>;
//   ^? string
Enter fullscreen mode Exit fullscreen mode

Notice the recursive call to Awaited<V> — this lets it unwrap nested promises. With every level, TypeScript peels off one Promise<> wrapper and infers the inner type, then recursively processes it.

This is so useful that TypeScript 4.5 shipped it as the built-in Awaited<T> type.


Pattern 4: Extract Function Parameters

You can infer more than just return types. The arguments tuple is fair game:

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

function createUser(
  name: string,
  age: number,
  role: "admin" | "user"
): void {
  // ...
}

type CreateUserArgs = Parameters<typeof createUser>;
//   ^? [name: string, age: number, role: "admin" | "user"]

// Need just the first parameter? Narrow the inference:
type FirstParam<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;

type Name = FirstParam<typeof createUser>;
//   ^? string

// Need the second parameter?
type SecondParam<T> = T extends (first: any, second: infer S, ...rest: any[]) => any ? S : never;

type Age = SecondParam<typeof createUser>;
//   ^? number
Enter fullscreen mode Exit fullscreen mode

This is how TypeScript's built-in Parameters<T> utility type works under the hood.


Pattern 5: Extract from Constructor Signatures

Same idea, but for classes and constructable objects:

type InstanceType<T> = T extends new (...args: any[]) => infer R ? R : never;

class Database {
  constructor(private url: string, private poolSize: number) {}
  connect() { /* ... */ }
}

type Db = InstanceType<typeof Database>;
//   ^? Database

// Constructor parameters:
type ConstructorParams<T> = T extends new (...args: infer P) => any ? P : never;

type DbParams = ConstructorParams<typeof Database>;
//   ^? [url: string, poolSize: number]
Enter fullscreen mode Exit fullscreen mode

Built-in InstanceType<T> and ConstructorParameters<T> use exactly this pattern.


Pattern 6: Multiple Inferences in One Check

You're not limited to a single infer — TypeScript can extract multiple pieces from one type:

type ExtractFunctionSignature<T> = T extends (
  ...args: infer P
) => infer R
  ? { params: P; returnType: R }
  : never;

function calculate(x: number, y: number): number {
  return x + y;
}

type Sig = ExtractFunctionSignature<typeof calculate>;
//   ^? { params: [x: number, y: number]; returnType: number }
Enter fullscreen mode Exit fullscreen mode

This is incredibly useful for metaprogramming — wrapping functions, building middleware, or creating type-safe event systems.


Pattern 7: String Template Literal Extraction

Combined with template literal types (TypeScript 4.1+), infer becomes even more powerful:

// Extract route parameter names from a URL pattern
type ExtractRouteParams<T extends string> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? { [K in Param | keyof ExtractRouteParams<Rest>]: string }
    : T extends `${string}:${infer Param}`
      ? { [K in Param]: string }
      : {};

type UserRoute = ExtractRouteParams<"/users/:id">;
//   ^? { id: string }

type PostRoute = ExtractRouteParams<"/posts/:postId/comments/:commentId">;
//   ^? { postId: string; commentId: string }
Enter fullscreen mode Exit fullscreen mode

TypeScript recursively infers parameter names from the string pattern. Each :paramName segment gets extracted as a string-typed property. Frameworks like tRPC and Hono use variations of this pattern.


Pattern 8: Discriminated Union Resolution

Need to resolve a union to the correct member based on its discriminant? infer handles it:

type EventMap = {
  click: { x: number; y: number };
  keydown: { key: string; ctrlKey: boolean };
  focus: { element: HTMLElement };
};

// Extract the payload for a specific event type
type EventPayload<T extends keyof EventMap> = EventMap[T];
// Simple indexed access

// Or: extract from a union of { type: string, payload: infer P }
type ExtractByType<T extends { type: string }, Discriminant extends T['type']> =
  T extends { type: Discriminant; payload: infer P } ? P : never;

type UserEvent =
  | { type: "created"; payload: { id: number } }
  | { type: "updated"; payload: { id: number; changes: string[] } }
  | { type: "deleted"; payload: { id: number } };

type CreatedPayload = ExtractByType<UserEvent, "created">;
//   ^? { id: number }
Enter fullscreen mode Exit fullscreen mode

When NOT to Use infer

infer is powerful, but it isn't the right tool for every problem:

Problem Use infer? Better alternative
Extract return type of a specific function ✅ Yes ReturnType<typeof fn> (uses infer internally)
Unwrap Promise value ✅ Yes Awaited<T> (uses infer internally)
Extract parameters of a callback ✅ Yes Parameters<T>
Make a property optional ❌ No Partial<T>, mapped types
Pick specific keys from an object ❌ No Pick<T, K>
Transform object values ❌ No Mapped types { [K in keyof T]: ... }
Enforce minimum/maximum length tuples ❌ No Variadic tuple types [T, ...T[]]

If you find yourself nesting infer more than 3 levels deep, step back. There's often a simpler approach with mapped types, indexed access types, or union types.


Common Gotchas

1. infer only works in conditional types. This is the #1 mistake:

// ❌ This is NOT valid:
type Extract<T, infer R> = R;

// ✅ This is:
type Extract<T> = T extends SomeType<infer R> ? R : never;
Enter fullscreen mode Exit fullscreen mode

2. Distributive conditional types can surprise you. When you pass a union to a conditional type with a naked type parameter, each union member is checked independently:

type Element<T> = T extends (infer E)[] ? E : never;

type Result = Element<string[] | number[]>;
// ^? string | number  — distributed over the union
Enter fullscreen mode Exit fullscreen mode

3. Recursive infer has depth limits. TypeScript caps recursive conditional type instantiation at 50 levels by default (configurable with --recursiveTypeDepth in TypeScript 5.6+). If you hit instantiation depth errors, restructure your types instead of bumping the limit. Don't use infer recursion for deeply nested object structures — mapped types handle that better.

4. infer can't "invent" types. It can only extract what's already structurally present:

// ❌ This won't infer 'bar' because there's nothing providing that shape
type ExtractBar<T> = T extends { bar: infer R } ? R : never;
type Test = ExtractBar<{ foo: string }>; // never (correctly!)
Enter fullscreen mode Exit fullscreen mode

The Takeaway

infer is TypeScript's pattern-matching tool for types. It's what makes advanced utility types (ReturnType, Parameters, Awaited, ConstructorParameters) possible, and it's the foundation for type-level metaprogramming in modern TypeScript.

The mental model is simple: declare a type variable inside the pattern match, and TypeScript fills it in.

Start with the basic patterns — return types, array elements, Promise values — and work up. Once infer clicks, you'll start seeing type extraction opportunities everywhere. And you'll stop manually duplicating types that the compiler already knows.


Article by Kai Thorne. Write type-safe code, not type-loud code.

Top comments (0)