Recursive Types

Difficulty: Advanced

Question

What are recursive types in TypeScript? Show practical examples of self-referencing type definitions.

Answer

Recursive types are types that reference themselves in their definition. They model data structures that are inherently recursive: trees, nested objects, linked lists, and JSON.

TypeScript supports recursion in: - Type aliases: type Tree<T> = { value: T; children: Tree<T>[] } - Conditional types: type Deep<T> = T extends object ? { [K in keyof T]: Deep<T[K]> } : T - Template literal types: type Path = ... (with depth limits)

Recursive conditional types (TS 4.1+) enable deep transformations like DeepPartial, DeepReadonly, and type-safe dot-path access.

Code examples

Recursive Data Structures

// JSON type - fully recursive
type JSONValue =
  | string
  | number
  | boolean
  | null
  | JSONValue[]
  | { [key: string]: JSONValue };

const data: JSONValue = {
  name: 'Alice',
  age: 30,
  hobbies: ['reading', 'coding'],
  address: {
    city: 'NYC',
    coordinates: [40.7, -74.0],
  },
};

// Tree structure
interface TreeNode<T> {
  value: T;
  children: TreeNode<T>[];
}

const fileSystem: TreeNode<string> = {
  value: 'root',
  children: [
    {
      value: 'src',
      children: [
        { value: 'index.ts', children: [] },
        { value: 'utils.ts', children: [] },
      ],
    },
    { value: 'README.md', children: [] },
  ],
};

// Linked list
type LinkedList<T> = {
  value: T;
  next: LinkedList<T> | null;
};

const list: LinkedList<number> = {
  value: 1,
  next: { value: 2, next: { value: 3, next: null } },
};

Recursive types naturally model hierarchical data. The key is that the type references itself as a property type, creating potentially infinite nesting.

Deep Utility Types

// DeepPartial - makes all nested properties optional
type DeepPartial<T> = T extends object
  ? { [K in keyof T]?: DeepPartial<T[K]> }
  : T;

// DeepReadonly - makes all nested properties readonly
type DeepReadonly<T> = T extends object
  ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
  : T;

// DeepRequired - makes all nested properties required
type DeepRequired<T> = T extends object
  ? { [K in keyof T]-?: DeepRequired<T[K]> }
  : T;

interface Config {
  server: {
    host: string;
    port: number;
    ssl?: {
      cert: string;
      key: string;
    };
  };
  database: {
    url: string;
    pool?: { min?: number; max?: number };
  };
}

// All nested fields optional for merging defaults
type PartialConfig = DeepPartial<Config>;
const overrides: PartialConfig = {
  server: { port: 8080 },        // OK: everything else optional
  database: { pool: { max: 20 } } // OK: deeply partial
};

// All nested fields readonly for immutability
type FrozenConfig = DeepReadonly<Config>;
// frozenConfig.server.port = 3000; // Error at any depth

Deep utility types use recursive conditional types to transform nested structures. DeepPartial is essential for config merging and partial updates of complex objects.

Recursive Type-Level Computation

// Flatten nested arrays recursively
type DeepFlatten<T> = T extends (infer U)[]
  ? DeepFlatten<U>
  : T;

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

// Get all leaf paths of an object as dot-notation strings
type Paths<T, Prefix extends string = ''> = T extends object
  ? {
      [K in keyof T & string]: T[K] extends object
        ? Paths<T[K], `${Prefix}${K}.`>
        : `${Prefix}${K}`;
    }[keyof T & string]
  : never;

interface AppState {
  user: { name: string; age: number };
  settings: { theme: string; lang: string };
}

type StatePaths = Paths<AppState>;
// 'user.name' | 'user.age' | 'settings.theme' | 'settings.lang'

// Type-safe getter using recursive path
type GetByPath<T, P extends string> =
  P extends `${infer Key}.${infer Rest}`
    ? Key extends keyof T
      ? GetByPath<T[Key], Rest>
      : never
    : P extends keyof T
      ? T[P]
      : never;

type NameType = GetByPath<AppState, 'user.name'>; // string
type AgeType = GetByPath<AppState, 'user.age'>;   // number

Recursive types with template literals enable type-safe dot-notation access. The Paths type generates all valid paths, and GetByPath resolves the type at a given path.

Key points

Concepts covered

Recursive Types, Self-Referencing Types, Deep Utilities, JSON Type, Tree Structures