TypeScript infer Keyword: Type Extraction in Conditionals

TypeScript's conditional types let you create types that branch based on type relationships. The basic syntax `T extends U ? X : Y` works well for simple checks, but what if you need to extract a...

Key Insights

  • The infer keyword enables pattern matching on types within conditional types, allowing you to extract and capture type information from complex generic structures
  • Use infer to build powerful utility types that unwrap Promises, extract function parameters and return types, and manipulate tuple types—capabilities that are impossible with basic generics alone
  • Multiple infer declarations in a single conditional type enable sophisticated type transformations like tuple destructuring and recursive type extraction, but watch for distribution behavior with union types

Understanding the Problem: Why We Need Type Extraction

TypeScript’s conditional types let you create types that branch based on type relationships. The basic syntax T extends U ? X : Y works well for simple checks, but what if you need to extract a piece of type information from within a generic type?

Consider trying to get the return type of a function without infer:

// We want to extract the return type, but how?
type GetReturnType<T> = T extends (...args: any[]) => any ? /* what goes here? */ : never;

// We can check IF it's a function, but we can't capture the return type
type IsFunction<T> = T extends (...args: any[]) => any ? true : false;

type Result = IsFunction<() => string>; // true, but we lost the string type

This is where infer comes in. It declares a type variable within the conditional’s extends clause, capturing type information for use in the true branch.

The infer Keyword: Syntax and Mechanics

The infer keyword follows this pattern:

T extends SomePattern<infer U> ? U : DefaultType

Here, U is a type variable that TypeScript will infer based on what matches that position in the pattern. If T matches the pattern, TypeScript extracts the type and makes it available as U in the true branch.

Let’s solve our return type problem:

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

type StringReturn = GetReturnType<() => string>; // string
type NumberReturn = GetReturnType<(x: number) => number>; // number
type NeverReturn = GetReturnType<string>; // never (not a function)

Extracting array element types works similarly:

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

type Num = ArrayElement<number[]>; // number
type Str = ArrayElement<string[]>; // string
type Multi = ArrayElement<(string | number)[]>; // string | number

The beauty of infer is that TypeScript does the hard work of figuring out what type fits in that position.

Common Patterns: Solving Real-World Type Problems

Unwrapping Promises

One of the most common uses of infer is extracting the resolved type from a Promise:

type Unwrap<T> = T extends Promise<infer U> ? U : T;

type A = Unwrap<Promise<string>>; // string
type B = Unwrap<Promise<number>>; // number
type C = Unwrap<string>; // string (already unwrapped)

For nested Promises, you can make it recursive:

type DeepUnwrap<T> = T extends Promise<infer U> ? DeepUnwrap<U> : T;

type Nested = DeepUnwrap<Promise<Promise<Promise<string>>>>; // string

Extracting Function Parameter Types

Getting all parameters as a tuple:

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

function greet(name: string, age: number) {
  return `${name} is ${age}`;
}

type GreetParams = Parameters<typeof greet>; // [string, number]

Getting just the first parameter:

type FirstParameter<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;

type FirstParam = FirstParameter<typeof greet>; // string

Extracting Object Property Types

You can use infer with mapped types to extract property types:

type PropertyType<T, K extends keyof T> = T extends { [key in K]: infer V } ? V : never;

interface User {
  name: string;
  age: number;
  active: boolean;
}

type NameType = PropertyType<User, 'name'>; // string
type AgeType = PropertyType<User, 'age'>; // number

Flattening Nested Arrays

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

type Deep = number[][][];
type Flat = Flatten<Deep>; // number

Advanced Patterns: Multiple infer Declarations

You can use multiple infer keywords in a single conditional type to extract multiple pieces of type information simultaneously.

Tuple Destructuring

Extract the first element and the rest:

type Head<T> = T extends [infer First, ...any[]] ? First : never;
type Tail<T> = T extends [any, ...infer Rest] ? Rest : never;

type Tuple = [string, number, boolean];
type First = Head<Tuple>; // string
type Rest = Tail<Tuple>; // [number, boolean]

Function Signature Decomposition

Extract both parameters and return type:

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

type Parts = FunctionParts<(x: string, y: number) => boolean>;
// { params: [string, number]; return: boolean }

Recursive Type Extraction

Build a type that reverses a tuple:

type Reverse<T extends any[]> = T extends [infer First, ...infer Rest]
  ? [...Reverse<Rest>, First]
  : [];

type Original = [1, 2, 3, 4];
type Reversed = Reverse<Original>; // [4, 3, 2, 1]

Building Utility Types with infer

Let’s implement some of TypeScript’s built-in utility types to understand how they work.

Custom ReturnType

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

function createUser(name: string) {
  return { name, id: Math.random() };
}

type User = MyReturnType<typeof createUser>; // { name: string; id: number }

Custom Awaited

type MyAwaited<T> = T extends Promise<infer U>
  ? U extends Promise<any>
    ? MyAwaited<U>
    : U
  : T;

type A = MyAwaited<Promise<string>>; // string
type B = MyAwaited<Promise<Promise<number>>>; // number

ConstructorParameters for Classes

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

class Database {
  constructor(public host: string, public port: number) {}
}

type DbParams = MyConstructorParameters<typeof Database>; // [string, number]

Pitfalls and Best Practices

Union Type Distribution

Conditional types distribute over unions, which affects infer:

type UnwrapArray<T> = T extends Array<infer U> ? U : T;

type Result = UnwrapArray<string[] | number[]>; // string | number

// This distributes as:
// UnwrapArray<string[]> | UnwrapArray<number[]>
// which becomes: string | number

To prevent distribution, wrap the type in a tuple:

type NoDistribute<T> = [T] extends [Array<infer U>] ? U : T;

type Result2 = NoDistribute<string[] | number[]>; // string | number (still distributes in the infer)

Inference in Covariant Positions

TypeScript infers to the union of all candidates when infer appears multiple times in covariant positions:

type Union<T> = T extends { a: infer U; b: infer U } ? U : never;

type Result = Union<{ a: string; b: number }>; // string | number

In contravariant positions (function parameters), it infers to the intersection:

type Intersect<T> = T extends {
  a: (x: infer U) => void;
  b: (x: infer U) => void;
} ? U : never;

type Result = Intersect<{
  a: (x: string) => void;
  b: (x: number) => void;
}>; // string & number (which is never)

Performance Considerations

Deeply recursive types with infer can slow down type checking. TypeScript has recursion limits (typically 50 levels). For complex scenarios, consider:

// Instead of unbounded recursion
type DeepFlatten<T> = T extends (infer E)[] ? DeepFlatten<E> : T;

// Use a depth limit
type DeepFlattenLimited<T, Depth extends number = 5> = Depth extends 0
  ? T
  : T extends (infer E)[]
  ? DeepFlattenLimited<E, [-1, 0, 1, 2, 3, 4][Depth]>
  : T;

Conclusion

The infer keyword unlocks TypeScript’s type-level programming capabilities. It transforms conditional types from simple type guards into powerful pattern-matching tools that can extract, transform, and manipulate types in sophisticated ways.

Use infer when you need to capture type information from generic types—whether that’s unwrapping Promises, extracting function signatures, manipulating tuples, or building custom utility types. The patterns shown here form the foundation of advanced TypeScript libraries and enable the kind of type safety that makes TypeScript invaluable for large-scale applications.

Start with simple extractions like return types and array elements, then progress to multiple infer declarations and recursive patterns as your needs grow. Just remember to watch for union distribution behavior and keep recursion depth reasonable for compilation performance.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.