Recursive Types
debt(d5/e3/b3/t5)
Closest to 'specialist tool catches it' (d5). The detection_hints list TypeScript itself (the compiler/type checker) as the sole tool, and automated detection is marked 'no'. Bare recursive aliases or excessively deep instantiation surface as compiler errors or 'Type instantiation is excessively deep' errors — caught by the TypeScript compiler when you actually invoke the type, not by a default linter pass. The code_pattern hint (any[]) suggests misuse via fallback to any is a secondary signal, also caught by tsc or strict mode rather than a plain linter.
Closest to 'simple parameterised fix' (e3). The quick_fix prescribes switching to interface for simple cases, or wrapping the recursive reference behind an object property or array boundary — a targeted, localised change within the type definition itself. For the any[] anti-pattern it recommends adopting existing library types (ts-essentials). None of these span multiple files or require architectural rework; they are small, focused corrections within one type definition or one component.
Closest to 'localised tax' (b3). Recursive types apply to web and cli contexts but are typically confined to specific type definition files or utility type modules. They don't impose a gravitational pull on the entire codebase; only the code that consumes those types is affected. If a DeepReadonly or Json recursive type is wrong it impacts consumers, but the fix is still contained to the type definition and its direct usages rather than being a cross-cutting architectural burden.
Closest to 'notable trap' (t5). The misconception field states developers believe recursive types always cause infinite compiler loops, when in fact only unconstrained instantiation does. The common mistake of writing a bare recursive alias (type T = T | string) triggers a compiler error, while the correct pattern (behind an object/array boundary) works fine. This is a documented gotcha that most TypeScript developers eventually learn, matching the t5 anchor — a real but learnable trap rather than a catastrophic one.
Also Known As
TL;DR
Explanation
TypeScript supports recursive type aliases and interfaces. Interface recursion has always worked: interface TreeNode { value: number; children: TreeNode[] }. Type alias recursion requires the recursive part to be behind an object/array (not a bare generic): type Json = string | number | boolean | null | Json[] | { [k: string]: Json }. Recursive conditional types (type DeepReadonly<T>) use conditional + infer to transform nested structures. Mutual recursion (A references B, B references A) works with interfaces. Pitfall: infinite instantiation — types like type Infinite<T> = Infinite<T[]> cause the compiler to loop.
Diagram
flowchart TD
JSON[Json type]
JSON --> STR[string]
JSON --> NUM[number]
JSON --> BOOL[boolean]
JSON --> NULL[null]
JSON --> ARR[Json array]
JSON --> OBJ[object Json values]
ARR --> JSON
OBJ --> JSON
Watch Out
Common Misconception
Why It Matters
Common Mistakes
- Bare recursive type alias without an object/array boundary — type T = T | string is disallowed; wrap in an array or object.
- Creating infinitely deep conditional types that cause 'Type instantiation is excessively deep' errors.
- Using any[] for JSON values instead of a proper recursive Json type.
- Not using interface for simple recursive structures — interfaces handle recursion more efficiently than type aliases.
Avoid When
- When the nesting depth is always finite and known — a fixed set of nested types is clearer.
- When recursive conditional types cause 'excessively deep' compiler errors — switch to interfaces.
When To Use
- Typing JSON, abstract syntax trees, file system trees, nested menu structures, or linked lists.
- Building recursive utility types like DeepReadonly, DeepPartial, or DeepRequired.
Code Examples
// Inaccurate — any allows anything, no structure
type JsonValue = any;
type TreeNode = { value: number; children: any[] };
// Recursive type alias for JSON
type Json =
| string | number | boolean | null
| Json[]
| { [key: string]: Json };
// Recursive interface for a tree
interface TreeNode<T> {
value: T;
children: TreeNode<T>[];
}
// Recursive conditional type — deep readonly
type DeepReadonly<T> = T extends (infer U)[]
? ReadonlyArray<DeepReadonly<U>>
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;