Skip to content

TypeScript Recursive Types — Complete Guide

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about TypeScript Recursive Types. We cover key concepts, practical examples, and best practices to help you master this topic.

TypeScript recursive types let you define types that reference themselves — essential for modeling hierarchical data like JSON, file systems, ASTs, and deeply nested configurations with full type safety.

What You'll Learn

  • Recursive type syntax and patterns
  • JSON type definition
  • Tree and Linked List types
  • Deep Partial and Deep Readonly utilities
  • Recursive conditional types

Why It Matters

Many real-world data structures are recursive — an HTML element contains child elements, a JSON value can contain other JSON values, a comment can have nested replies. Without recursive types, you'd need a fixed maximum depth or resort to any.

Real-World Use

Durga Antivirus Pro's configuration system uses a DeepPartial type for partial configuration overrides — users can specify only the settings they want to change, at any nesting level. The Doda Browser bookmarks API uses recursive types for bookmark trees.

Learning Path

flowchart LR
  A[Narrowing] --> B[Recursive Types]
  B --> C[Variance]
  B --> D[You Are Here]
  C --> E[Overloads]
  E --> F[Branded Types]

Recursive Type Basics

A recursive type references itself:

type TreeNode<T> = {
  value: T;
  children: TreeNode<T>[];
};

const tree: TreeNode<number> = {
  value: 1,
  children: [
    {
      value: 2,
      children: [],
    },
    {
      value: 3,
      children: [
        {
          value: 4,
          children: [],
        },
      ],
    },
  ],
};

Think of a recursive type like a Russian nesting doll — each doll contains another doll of the same shape.

The JSON Type

A classic recursive type — JSON values can be any of several types, including nested JSON objects and arrays:

type JSONValue =
  | string
  | number
  | boolean
  | null
  | JSONValue[]
  | { [key: string]: JSONValue };

const config: JSONValue = {
  app: {
    name: "Scanner",
    version: 2.1,
    features: ["realTime", "autoUpdate"],
    debug: false,
  },
  nested: {
    deep: {
      value: null,
    },
  },
};

// Type-safe access with type guards
function isObject(value: JSONValue): value is { [key: string]: JSONValue } {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function getConfigValue(obj: JSONValue, path: string[]): JSONValue | undefined {
  let current = obj;
  for (const key of path) {
    if (isObject(current)) {
      current = current[key];
    } else {
      return undefined;
    }
  }
  return current;
}

Linked List

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

function createList<T>(values: T[]): LinkedList<T> | null {
  if (values.length === 0) return null;

  let head: LinkedList<T> = { value: values[0], next: null };
  let current = head;

  for (let i = 1; i < values.length; i++) {
    current.next = { value: values[i], next: null };
    current = current.next;
  }

  return head;
}

function traverse<T>(list: LinkedList<T> | null, fn: (value: T) => void): void {
  let current = list;
  while (current) {
    fn(current.value);
    current = current.next;
  }
}

const list = createList([1, 2, 3, 4, 5]);
traverse(list, console.log); // 1, 2, 3, 4, 5 (each on its own line)

File System Tree

type FileSystemNode = {
  name: string;
} & (
  | { type: "file"; size: number; extension: string }
  | { type: "directory"; children: FileSystemNode[] }
);

function getSize(node: FileSystemNode): number {
  if (node.type === "file") {
    return node.size;
  }
  return node.children.reduce((total, child) => total + getSize(child), 0);
}

function findFiles(node: FileSystemNode, extension: string): FileSystemNode[] {
  if (node.type === "file") {
    return node.extension === extension ? [node] : [];
  }
  return node.children.flatMap(child => findFiles(child, extension));
}

const root: FileSystemNode = {
  name: "root",
  type: "directory",
  children: [
    { name: "docs", type: "directory", children: [
      { name: "readme.md", type: "file", size: 1024, extension: "md" },
      { name: "guide.md", type: "file", size: 2048, extension: "md" },
    ]},
    { name: "src", type: "directory", children: [
      { name: "index.ts", type: "file", size: 512, extension: "ts" },
      { name: "utils.ts", type: "file", size: 768, extension: "ts" },
    ]},
  ],
};

console.log(`Total size: ${getSize(root)} bytes`); // Total size: 4352 bytes
console.log(`MD files: ${findFiles(root, "md").length}`); // MD files: 2

Deep Partial

Makes all properties — including nested ones — optional:

type DeepPartial<T> = T extends object
  ? T extends Function
    ? T
    : { [K in keyof T]?: DeepPartial<T[K]> }
  : T;

interface Config {
  server: {
    host: string;
    port: number;
    tls: {
      enabled: boolean;
      cert: string;
    };
  };
  database: {
    host: string;
    port: number;
    credentials: {
      user: string;
      password: string;
    };
  };
}

const partialConfig: DeepPartial<Config> = {
  server: {
    host: "localhost",
    tls: {
      enabled: true,
    },
  },
  // database is entirely optional
};

Deep Readonly

type DeepReadonly<T> = T extends object
  ? T extends Function
    ? T
    : { readonly [K in keyof T]: DeepReadonly<T[K]> }
  : T;

type ReadonlyConfig = DeepReadonly<Config>;
// All properties at all levels are readonly:
// { readonly server: { readonly host: string; readonly port: number; ... } }

Recursive Conditional Types

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

type NestedArray = [[1, 2], [3, [4, 5]], 6];
type Flat = Flatten<NestedArray>; // 1 | 2 | 3 | 4 | 5 | 6

Recursive Mapped Types

type DeepPick<T, K extends string> = T extends object
  ? {
      [P in keyof T as P extends K ? P : never]: DeepPick<T[P], K>;
    }
  : T;

Common Mistakes

1. Creating Types That Recursively Reference Without a Base Case

// Infinite recursion — no base case
type Infinite<T> = { value: T; next: Infinite<T> };

2. Exceeding TypeScript's Recursion Limit

TypeScript has a depth limit (typically ~50 levels). Very deep recursion (like deeply nested objects) may hit this limit.

3. Making Recursive Types Too Generic

// Too generic — catches everything
type Recursive<T> = T | Recursive<T>;
// TypeScript doesn't allow this kind of bare recursion

4. Forgetting Array Methods on Recursive Types

type Tree<T> = { value: T; children: Tree<T>[] };

function mapTree<T, U>(tree: Tree<T>, fn: (value: T) => U): Tree<U> {
  return {
    value: fn(tree.value),
    children: tree.children.map(child => mapTree(child, fn)),
  };
}

5. Not Handling Edge Cases

Always consider null, undefined, and circular references when working with recursive types at runtime.

Practice Questions

  1. What is a recursive type? A type that references itself directly or indirectly, enabling the definition of infinitely nested or hierarchical structures.

  2. What is the base case in a recursive type? The non-recursive branch that terminates the recursion — for example, string in JSONType or null in LinkedList<T> | null.

  3. Can recursive types work with generics? Yes: type Tree<T> = { value: T; children: Tree<T>[] }

  4. What is TypeScript's recursion limit? Typically 50 levels deep. This applies to recursive conditional types and instantiation depth.

Challenge: Define a recursive type for an HTML-like DOM tree where each node has a tagName, optional attributes, optional children (which are themselves DOM nodes), and optional textContent. Write a function to render it as an HTML string.

FAQ

Can recursive types cause infinite compilation?

TypeScript has recursion limits to prevent infinite loops. Deeply nested types beyond the limit cause compile errors.

Are recursive types supported in all TypeScript versions?

Recursive types have improved over versions. TypeScript 4.1+ handles them well. Earlier versions had stricter limits.

Can I have mutually recursive types?

Yes: type A = { b: B }; type B = { a: A };

Do recursive types affect compilation speed?

Deeply recursive types can slow Type Checking. Keep recursion depth reasonable.

Can recursive types be used with `typeof`?

Yes: const tree = { ... }; type Tree = typeof tree; works for inferred recursive structures.

Mini Project: Comment Thread System

// src/comment-system.ts

type Comment = {
  id: string;
  author: string;
  text: string;
  timestamp: Date;
  replies: Comment[];
};

function createComment(author: string, text: string): Comment {
  return {
    id: `comment-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
    author,
    text,
    timestamp: new Date(),
    replies: [],
  };
}

function addReply(parent: Comment, reply: Comment): void {
  parent.replies.push(reply);
}

function countComments(comment: Comment): number {
  let count = 1; // Count this comment
  for (const reply of comment.replies) {
    count += countComments(reply);
  }
  return count;
}

function findComment(comment: Comment, id: string): Comment | undefined {
  if (comment.id === id) return comment;
  for (const reply of comment.replies) {
    const found = findComment(reply, id);
    if (found) return found;
  }
  return undefined;
}

function renderComment(comment: Comment, indent: number = 0): string {
  const prefix = "  ".repeat(indent);
  let result = `${prefix}[${comment.author}] ${comment.text}\n`;

  for (const reply of comment.replies) {
    result += renderComment(reply, indent + 1);
  }

  return result;
}

const root = createComment("Alice", "This is a great tutorial!");
const reply1 = createComment("Bob", "I agree, very helpful!");
const reply2 = createComment("Charlie", "Can you explain line 42?");

addReply(root, reply1);
addReply(root, reply2);

const nestedReply = createComment("Alice", "Sure! Line 42 does...");
addReply(reply2, nestedReply);

console.log(renderComment(root));
console.log(`Total comments: ${countComments(root)}`);
// [Alice] This is a great tutorial!
//   [Bob] I agree, very helpful!
//   [Charlie] Can you explain line 42?
//     [Alice] Sure! Line 42 does...

// Total comments: 4

What's Next

Now explore variance in TypeScript's type system:

Lesson Description
{{< ref "/programming-languages/typescript/26-narrowing" >}} Review narrowing
{{< ref "/programming-languages/typescript/28-variance" >}} Covariance, contravariance, bivariance
{{< ref "/programming-languages/typescript/29-overloads-hybrid" >}} Function and constructor overloads

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro