Recursive Types & Their Limits

Advanced · 20 min read · ▶ live playground · ✦ checkpoint

Recursive types are types that refer back to themselves. They let TypeScript describe shapes that repeat: nested patches, JSON values, trees, and strings that can be parsed segment by segment.

Use them carefully. Recursion is one of the places where the type layer can become powerful enough to slow the checker, confuse teammates, or both.

DeepPartial

A top-level Partial<T>-style mapped type makes only the top-level properties optional. A recursive version can keep walking into nested objects and arrays.

This teaching copy is intentionally small. It is meant for ordinary data objects, not every JavaScript value ever created.

typescript — playgroundtype-checked
⌘/Ctrl + Enter to run

Read the alias in three branches:

  • If T is an array, recurse into the item type and return an array of partial items.
  • If T is an object, map every key and recursively make each value partial.
  • Otherwise, keep the primitive or literal value type as-is.

That last branch is why the break-it line fails. beta can be omitted, but when you provide it, it still has to be boolean.

JSON Is Recursive Too

JSON is the classic recursive data shape. A JSON value can be a primitive, an array of JSON values, or an object whose property values are JSON values.

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

The alias points back to itself through arrays and objects. That is not a loop at runtime. It is a type-level description of a repeating shape.

typescript — playgroundtype-checked
⌘/Ctrl + Enter to run

Recursive types describe possible structure. They still do not validate unknown data coming from outside your program; Section 13 owns that runtime boundary. Here, the value is authored in TypeScript, so the checker can inspect it directly.

Recursive String Parsing

Template literal types can recurse too. 11.3 extracted the first :param from a route. A recursive alias can keep scanning the rest of the path.

typescript — playgroundtype-checked
⌘/Ctrl + Enter to run

This is still shallow enough to be readable: find one :param, union it with the recursive result for the rest of the path, stop when no parameter remains. It is a useful example, but it is also the line where you should start watching the complexity budget.

Tail Shape And The Ceiling

The checker has protections because recursive types can otherwise ask it to work forever. When a conditional type gets too deep or expands too many intermediate types, TypeScript can stop with TS2589: Type instantiation is excessively deep and possibly infinite.

One way to make recursive types friendlier is to write them in a tail-recursive shape: keep an accumulator parameter, and make the recursive call the whole result of the branch.

type Segments<Path extends string, Seen extends string[] = []> =
  Path extends `${infer Head}/${infer Rest}`
    ? Segments<Rest, [...Seen, Head]>
    : [...Seen, Path];
 
type Example = Segments<"users/:id/posts/:postId">;

Seen carries the work already done. Rest gets smaller each step. Modern TypeScript can optimize some of these tail-shaped conditional types better than recursive types that keep wrapping the result after the recursive call.

A Complexity Budget

Here is the rule of thumb for production code: recursive types must earn their keep twice — first with the checker, then with the humans reading them.

Use a recursive type when the source domain is genuinely recursive: JSON, trees, nested patches, or route strings with repeated segments. Avoid recursion when two named aliases or one explicit object type would be clearer.

Keep the budget small:

  • Prefer one recursive alias per feature.
  • Name intermediate pieces instead of nesting every trick in one line.
  • Keep generated unions small enough that hover output is useful.
  • Test the worst realistic input with tsc --noEmit, not just the happy example.
  • Delete type magic when it mostly proves that you can write type magic.

You now have the mechanics: inspect types, branch on them, infer pieces, remap keys, generate strings, and recurse when the shape repeats. Section 12 changes mode: instead of building every transformer by hand, you will use TypeScript's standard utility types pragmatically.

Checkpoint

Answer all three to mark this lesson complete

+50 XP on completion