⚡IntermediateAdvanced Type System
Recursive Types
Model trees, nested structures, and linked lists at the type level using self-referential type aliases and recursive conditional types.
Overview
A **recursive type** is a type alias that references itself. This lets you describe data structures that are nested to an arbitrary depth - trees, JSON, linked lists, file system paths.
TypeScript 3.7+ supports recursive type aliases directly: `type Tree<T> = { value: T; children: Tree<T>[] }`. Before that, you needed an interface to get recursion (interfaces have always supported self-reference via structural typing).
Recursive **conditional types** go further: they apply conditional logic at each level of nesting. `DeepReadonly<T>` is the classic example - if a property is itself an object, recurse into it; if it's a primitive, apply readonly and stop.
The key constraint is that TypeScript must be able to determine the recursion terminates. Most practical patterns (deepening an object, unwrapping arrays, traversing a tree) do terminate naturally. TypeScript enforces this and will error if it detects a truly infinite expansion with no base case.
Key Points
- •Recursive type alias: `type Node<T> = { value: T; next: Node<T> | null }` - refers to itself
- •Works for object types, arrays, tuples, and union members
- •Recursive conditional types: apply a transformation recursively until hitting a base case (primitives)
- •`DeepReadonly<T>`, `DeepPartial<T>`, `DeepRequired<T>` are all built with recursive conditional types
- •JSON type: `type JSON = string | number | boolean | null | JSON[] | { [key: string]: JSON }` - a real recursive type
- •Depth-limiting: TypeScript enforces termination but has practical depth limits; very deep recursion may cause "Type instantiation is excessively deep" errors
- •Prefer recursive interfaces over recursive type aliases when describing object shapes - interfaces are more efficient
Examples
Tree and linked list types
// Binary tree
type TreeNode<T> = {
value: T;
left: TreeNode<T> | null;
right: TreeNode<T> | null;
};
const tree: TreeNode<number> = {
value: 1,
left: {
value: 2,
left: { value: 4, left: null, right: null },
right: { value: 5, left: null, right: null },
},
right: {
value: 3,
left: null,
right: { value: 6, left: null, right: null },
},
};
// Linked list (recursive interface - more efficient)
interface ListNode<T> {
value: T;
next: ListNode<T> | null;
}
const list: ListNode<string> = {
value: 'first',
next: {
value: 'second',
next: { value: 'third', next: null },
},
};
// Traversal helpers (type-safe)
function treeSum(node: TreeNode<number> | null): number {
if (!node) return 0;
return node.value + treeSum(node.left) + treeSum(node.right);
}
console.log(treeSum(tree)); // 21JSON type and nested data
// The famous recursive JSON type
type JSONValue =
| string
| number
| boolean
| null
| JSONValue[]
| { [key: string]: JSONValue };
// Safely describe any valid JSON
const config: JSONValue = {
name: 'my-app',
version: 1,
settings: {
debug: true,
tags: ['ts', 'json'],
nested: { deep: { deeper: null } },
},
};
// Safe deep-access utility
function get(obj: JSONValue, path: string[]): JSONValue {
let current = obj;
for (const key of path) {
if (typeof current !== 'object' || current === null || Array.isArray(current)) {
return null;
}
current = (current as { [k: string]: JSONValue })[key] ?? null;
}
return current;
}
console.log(get(config, ['settings', 'tags'])); // ['ts', 'json']Recursive conditional types (DeepReadonly, DeepPartial)
// DeepReadonly - make every nested property readonly
type DeepReadonly<T> =
T extends (infer U)[]
? ReadonlyArray<DeepReadonly<U>>
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
// DeepPartial - make every nested property optional
type DeepPartial<T> =
T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
interface Config {
server: { host: string; port: number; ssl: { enabled: boolean } };
database: { url: string; pool: { min: number; max: number } };
}
type FrozenConfig = DeepReadonly<Config>;
// Every property at every level is readonly
type PartialConfig = DeepPartial<Config>;
// Every property at every level is optional
const patch: PartialConfig = {
server: { port: 8080 }, // only patch port, leave the rest
};
const frozen: FrozenConfig = {
server: { host: 'localhost', port: 3000, ssl: { enabled: true } },
database: { url: 'postgres://...', pool: { min: 2, max: 10 } },
};
// frozen.server.port = 4000; // Error: readonlyCommon Mistakes
- "Type instantiation is excessively deep" error - TypeScript has a recursion depth limit (~100 levels). Simplify the type or add an early exit branch for large depths
- Recursive type aliases that directly expand to themselves without an object/array wrapper cause infinite loops - always have a union base case like `| null` or `| Primitive`
- Using recursive conditionals on union types can cause unexpected distribution - wrap in a tuple `[T] extends [object]` to prevent distribution when needed
Note
Every recursive type needs a base case - a branch that does NOT recurse. For `DeepReadonly<T>`, the base case is "T is a primitive, return T as-is". Without a base case, TypeScript either errors or enters infinite expansion. Think of it like a recursive function: always have a return path that doesn't call itself.
Exercises
Write TypeScript code to solve each exercise. Click Run to compile and execute it right here, then mark as done.
Loading editor…
Completed: 0 / 3 exercises
Knowledge Check
Question 1 of 4