Skip to content
Codeloom
TypeScript

Advanced Conditional Types With infer in TypeScript

Go beyond basic conditional types with infer, recursive conditionals, and distributive patterns to build powerful type-level programs.

·6 min read · By Codeloom
Advanced 10 min read

What you'll learn

  • Use infer to extract types from complex structures
  • Build recursive conditional types for deep transformations
  • Control distributive behavior with wrapping tricks

Prerequisites

  • Basic TypeScript generics
  • Familiarity with union and intersection types

Conditional types are TypeScript’s if-else at the type level. The basic form T extends U ? X : Y is straightforward, but the real power comes from infer, recursion, and distribution control. This article covers the advanced patterns that let you build type-level parsers, transformers, and validators.

Quick Review: The Basics

A conditional type checks if a type extends another.

type IsString<T> = T extends string ? true : false;

type A = IsString<'hello'>; // true
type B = IsString<42>;      // false

When T is a union, the conditional distributes over each member.

type C = IsString<'hello' | 42>; // true | false => boolean

The infer Keyword

The infer keyword declares a type variable inside a conditional type’s extends clause. TypeScript infers its value from the matched structure.

// Extract the return type of a function
type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;

type A = ReturnOf<() => string>;          // string
type B = ReturnOf<(x: number) => boolean>; // boolean

You can infer from any structural position: array elements, promise values, object properties, template literals.

// Extract the element type of an array
type ElementOf<T> = T extends readonly (infer E)[] ? E : never;

type C = ElementOf<string[]>;        // string
type D = ElementOf<[number, string]>; // number | string

// Unwrap a promise
type Awaited<T> = T extends Promise<infer V> ? Awaited<V> : T;

type E = Awaited<Promise<Promise<string>>>; // string

Multiple infer Positions

You can use infer in multiple positions within the same conditional.

type FirstAndLast<T extends readonly unknown[]> =
  T extends readonly [infer First, ...unknown[], infer Last]
    ? [First, Last]
    : T extends readonly [infer Only]
      ? [Only, Only]
      : never;

type A = FirstAndLast<[1, 2, 3, 4]>; // [1, 4]
type B = FirstAndLast<['only']>;       // ['only', 'only']

You can also infer from function parameters.

type SecondParam<T> =
  T extends (a: any, b: infer B, ...rest: any[]) => any ? B : never;

type P = SecondParam<(x: string, y: number, z: boolean) => void>;
// number

Recursive Conditional Types

TypeScript 4.1 added support for recursive conditional types. This lets you process arbitrarily deep structures.

// Deep readonly: makes every nested property readonly
type DeepReadonly<T> =
  T extends Function
    ? T
    : T extends object
      ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
      : T;

interface Nested {
  a: {
    b: {
      c: string;
    };
    d: number[];
  };
}

type ReadonlyNested = DeepReadonly<Nested>;
// All properties at every level are readonly

Here is a type that flattens nested arrays.

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

type A = Flatten<number[][][]>; // number
type B = Flatten<string[]>;      // string
type C = Flatten<boolean>;       // boolean

Building a Type-Level Parser

Combining infer with template literal types lets you parse strings at the type level.

// Parse a comma-separated string into a tuple
type ParseCSV<S extends string> =
  S extends `${infer Head},${infer Tail}`
    ? [Head, ...ParseCSV<Tail>]
    : [S];

type Parsed = ParseCSV<'a,b,c,d'>;
// ['a', 'b', 'c', 'd']

// Parse key-value pairs from a query string
type ParseQuery<S extends string> =
  S extends `${infer Key}=${infer Value}&${infer Rest}`
    ? { [K in Key]: Value } & ParseQuery<Rest>
    : S extends `${infer Key}=${infer Value}`
      ? { [K in Key]: Value }
      : {};

type Query = ParseQuery<'name=alice&age=30&role=admin'>;
// { name: 'alice' } & { age: '30' } & { role: 'admin' }

Controlling Distribution

By default, conditional types distribute over unions when the checked type is a naked type parameter. Sometimes you want to prevent this.

// Distributive: checks each union member separately
type IsStringDist<T> = T extends string ? 'yes' : 'no';
type A = IsStringDist<string | number>; // 'yes' | 'no'

// Non-distributive: wrapping in a tuple prevents distribution
type IsStringAll<T> = [T] extends [string] ? 'yes' : 'no';
type B = IsStringAll<string | number>; // 'no'

The wrapping trick with [T] extends [U] is essential when you need to check a union as a whole rather than member by member.

// Check if a type is exactly never
type IsNever<T> = [T] extends [never] ? true : false;

type C = IsNever<never>;        // true
type D = IsNever<string>;       // false
type E = IsNever<string | never>; // false

Inferring in Constrained Positions

You can add constraints to infer declarations since TypeScript 4.7.

type GetStringValues<T> =
  T extends Record<string, infer V extends string> ? V : never;

type A = GetStringValues<{ a: 'hello'; b: 'world' }>;
// 'hello' | 'world'

// Without the constraint, V would include non-string values
type GetValues<T> =
  T extends Record<string, infer V> ? V : never;

type B = GetValues<{ a: 'hello'; b: 42 }>;
// 'hello' | 42

Practical Example: Type-Safe API Response Handler

type ApiResponse<T> =
  | { status: 'success'; data: T }
  | { status: 'error'; message: string };

type ExtractData<T> =
  T extends ApiResponse<infer D> ? D : never;

type UserResponse = ApiResponse<{ id: string; name: string }>;
type UserData = ExtractData<UserResponse>;
// { id: string; name: string }

// Build a handler map from a response union
type HandlerMap<T extends { status: string }> = {
  [S in T['status']]: (
    response: Extract<T, { status: S }>
  ) => void;
};

function handleResponse<T extends { id: string; name: string }>(
  response: ApiResponse<T>,
  handlers: HandlerMap<ApiResponse<T>>
) {
  handlers[response.status](response as any);
}

Type-Level Arithmetic (Tuple Counting)

You can use tuple types and recursion for type-level counting.

type Length<T extends readonly unknown[]> = T['length'];

type BuildTuple<N extends number, T extends unknown[] = []> =
  T['length'] extends N ? T : BuildTuple<N, [...T, unknown]>;

type Add<A extends number, B extends number> =
  Length<[...BuildTuple<A>, ...BuildTuple<B>]>;

type Sum = Add<3, 4>; // 7

This is a niche technique, but it demonstrates the computational power of TypeScript’s type system.

Recursion Depth Limits

TypeScript limits recursive type instantiation to about 1000 levels. If your recursive conditional type hits this limit, restructure it. Common techniques include tail-call optimization patterns with accumulator type parameters or breaking the recursion into smaller chunks.

// Tail-call style with accumulator
type Reverse<T extends unknown[], Acc extends unknown[] = []> =
  T extends [infer Head, ...infer Tail]
    ? Reverse<Tail, [Head, ...Acc]>
    : Acc;

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

Wrap Up

Advanced conditional types with infer let you extract, transform, and compute types from complex structures. Use recursive conditionals for deep transformations, control distribution with tuple wrapping, and constrain infer variables for precision. These patterns are the building blocks of type-level programming in TypeScript.