TypeScript Recursive Types in 2026: Modeling JSON, Trees, and Deep Partial Without Hitting the Limit A developer demonstrates how TypeScript recursive types can model deeply nested structures like JSON, trees, and deep partial objects without hitting the compiler's depth limit, preserving type safety at all levels. The approach uses a base case to terminate recursion and prevents runtime errors by catching path issues at compile time. This article was written with the assistance of AI, under human supervision and review. Most TypeScript codebases handle nested data—JSON responses, file trees, comment threads—by writing any or falling back to runtime validation. The compiler cannot protect operations on deeply nested structures because the types stop at one or two levels. When a frontend engineer fetches a configuration object with arbitrary nesting and tries to access config.theme.colors.primary.default , the type system offers no help. The runtime throws Cannot read property 'default' of undefined and the developer spends an hour tracing the shape. Problem flow where nested data loses type safety Recursive types solve this by letting the type system follow structure to any depth. A JSONValue type that references itself models arrays of objects containing arrays—TypeScript infers the entire shape and catches path errors at compile time. The compiler sees that config.theme.colors might be undefined and forces the developer to handle it before accessing .primary . Solution flow where recursive types maintain type safety at all depths This distinction is critical. Without recursive types, teams write defensive runtime checks everywhere or accept silent failures. With recursive types, the compiler enforces safety once in the type definition and every consumer benefits automatically. null , unknown to terminate recursion and prevent infinite expansion during type checking.A recursive type is a type that references itself within its own definition, enabling the compiler to model structures that nest indefinitely. The classic example is a linked list node where each node points to another node of the same type. TypeScript resolves these definitions by deferring the expansion—when the compiler encounters a recursive reference, it treats the type as a placeholder until the structure terminates with a base case. Recursive type resolution showing base case termination The key insight is that recursion in types mirrors recursion in runtime code. A recursive function needs a termination condition to avoid infinite loops; a recursive type needs a base case—typically a primitive or union with non-recursive branches—to avoid infinite expansion. Without a base case, the compiler attempts to expand the type forever and eventually hits the depth limit. TypeScript's structural type system means recursive types work seamlessly with inference. When a function accepts a recursive type parameter, the compiler walks the structure and infers the exact shape. This matters because developers can write type-safe operations on nested data without annotating every level manually. The recursion depth limit exists as a safety valve. In TypeScript 5.6 and beyond, the compiler allows approximately 50 nested expansions before throwing an error. Most real-world data structures—even heavily nested JSON or deep file trees—stay well below this threshold. The limit only becomes a problem when a type definition creates an infinite expansion or when conditional types cause exponential branching during inference. JSON supports primitives, arrays, and objects that can nest arbitrarily. The standard approach in TypeScript is to type JSON as any , losing all safety. A recursive type models the exact JSON specification while preserving type information at every level. type JSONPrimitive = string | number | boolean | null; type JSONValue = | JSONPrimitive | JSONValue | { key: string : JSONValue }; // Usage: parsing API responses with full type safety function processConfig config: JSONValue : void { if typeof config === "object" && config == null && Array.isArray config { // TypeScript knows config is { key: string : JSONValue } const theme = config.theme; if typeof theme === "object" && theme == null && Array.isArray theme { const colors = theme.colors; // Compiler enforces checking at each level } } } // Example: type-safe path access with narrowing const apiResponse: JSONValue = { user: { profile: { name: "Alice", preferences: { theme: "dark", notifications: true } } } }; This definition covers every valid JSON structure. The base case— JSONPrimitive —terminates recursion for leaf values. The recursive branches— JSONValue and { key: string : JSONValue } —allow nesting to any depth. TypeScript infers the exact type when you assign a literal object, but the recursive definition ensures the compiler accepts any conforming structure. The practical benefit shows when developers access nested properties. Without recursive types, accessing apiResponse.user.profile.name compiles but provides no safety—any misspelling or missing property fails at runtime. With JSONValue , the compiler forces type narrowing at each level. You must check that user exists and is an object before accessing profile . This requirement seems verbose, but it catches bugs that would otherwise manifest as production errors. Teams building API clients or configuration parsers use this pattern extensively. The type definition goes in a shared types file, and every function that consumes API data references JSONValue . The initial cost is a single recursive type; the payoff is eliminating an entire class of runtime failures across the codebase. Tree structures—DOM nodes, file systems, organizational charts—require recursive types to model parent-child relationships accurately. A file system node can be a file leaf or a directory containing more nodes branch . This maps directly to a recursive union type. type FileSystemNode = File | Directory; interface File { type: "file"; name: string; size: number; content: string; } interface Directory { type: "directory"; name: string; children: FileSystemNode ; } // Type-safe tree traversal function calculateSize node: FileSystemNode : number { if node.type === "file" { return node.size; } // TypeScript knows node is Directory here return node.children.reduce sum, child = sum + calculateSize child , 0 ; } // Example: modeling a project structure const projectTree: Directory = { type: "directory", name: "src", children: { type: "file", name: "index.ts", size: 1024, content: "export from './app';" }, { type: "directory", name: "components", children: { type: "file", name: "Button.tsx", size: 2048, content: "export const Button = =