Typorithm · hard

15. Recursive types, depth budgets and compiler cost

typescriptstable-typescriptinterviewindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

15. Recursive types, depth budgets and compiler cost

0%

Concept and contract

Use recursive transformations only with an explicit domain and practical limits.

A recursive type can describe nested data or repeatedly transform a structure. The useful question is which inputs it supports and how consumers benefit. A one-line DeepReadonly that treats every object alike often mishandles functions, special collections or host objects and can make diagnostics difficult to understand.

Begin with a deliberately small JSON-like domain and an explicit depth budget. At the budget boundary, choose a documented fallback rather than pretending the transformation continues forever. This makes the resulting guarantee understandable and bounds compiler work.

Mechanism and worked example

The example recursively collects leaf strings from a nested readonly array type.

The example recursively collects leaf strings from a nested readonly array type. Its domain excludes arbitrary object graphs, so the matching rule stays small. Type-level recursion describes possible shapes; it neither traverses a runtime value nor detects a cyclic JavaScript object.

Compiler instantiation limits are implementation constraints, not a stable business API. Large union expansions and recursive conditionals can consume substantial checking resources. Compare extendedDiagnostics on a representative project before and after a public type abstraction rather than assuming native compilation makes complexity free.

Complete worked example

typescript

type LeafText<T> = T extends readonly (infer U)[] ? LeafText<U> : T extends string ? T : never;
const leaf: LeafText<readonly ["QA", readonly ["PM"]]> = "QA";
Reasoning
The recursive rule handles only arrays of leaves and filters unsupported leaf kinds explicitly.

Counterexamples and common mistakes

The rejected assignment violates the extracted leaf domain.

The rejected assignment violates the extracted leaf domain. An any fallback would make it compile and conceal a transformation failure. A never fallback excludes unsupported members; an unknown fallback preserves uncertainty. Choose based on the public contract, not merely on suppressing diagnostics.

Do not teach recursive type arithmetic as a prerequisite to ordinary API work. Most application code needs simple mapped types and validated data. Keep advanced recursion as a bounded engineering choice and provide a concrete simpler alternative for readers who do not need it.

Rejected counterexample: expected compiler diagnostic

typescript

type LeafText<T> = T extends readonly (infer U)[] ? LeafText<U> : T extends string ? T : never;
// @ts-expect-error Numeric leaves are outside the supported result.
const invalid: LeafText<readonly ["QA", 2]> = 2;
Reasoning
The resulting leaf union excludes numeric values rather than weakening to any.

Independent practice

Implement a depth-two readonly view for a plain object with one nested plain object.

Implement a depth-two readonly view for a plain object with one nested plain object. First write the resulting concrete type manually, then generalize only the two supported levels. State that arrays, functions and special objects are outside this exercise’s model.

Reject a write to a nested scalar and preserve readable values. Optional extension: add a tuple-based depth budget and inspect compiler diagnostics on a larger shape. The extension is not required to finish the practical two-level transformation.

Step 1: Write the concrete two-level readonly output. Step 2: Map only those supported property levels.

Practice starter: solve before reading the solution

typescript

type Model = { title: string; details: { count: number } };
// Write the concrete readonly result, then derive a bounded helper.
Reasoning
Use a plain two-level model and state the unsupported cases before generalizing.
  • Hint 1: The helper is deliberately shallow beyond its stated depth.
  • Hint 2: Runtime aliases are not frozen.

Solution, tests and trade-offs

The solution maps the outer properties and makes one nested object layer readonly.

The solution maps the outer properties and makes one nested object layer readonly. It deliberately stops there rather than claiming universal deep immutability. Runtime objects remain ordinary objects, and another alias may still mutate them unless ownership is controlled separately.

Compile rejected writes at both covered levels and a positive read. Explain the behavior beyond the second level explicitly. A bounded helper is more honest than an unqualified DeepReadonly name whose implementation silently stops or mishandles unsupported types.

Practice solution and acceptance checks

typescript

type ReadonlyTwo<T> = { readonly [K in keyof T]: T[K] extends object ? Readonly<T[K]> : T[K] };
type Model = { title: string; details: { count: number } };
const model: ReadonlyTwo<Model> = { title: "QA", details: { count: 2 } };

// Acceptance checks
if(model.details.count!==2)throw new Error("bounded read");
Reasoning
The helper intentionally covers two property levels and does not freeze runtime aliases.

Interview reasoning and next step

In interviews, ask what objects and depth the transformation supports before implementing it.

In interviews, ask what objects and depth the transformation supports before implementing it. Discuss readable diagnostics, checker cost and a simpler concrete contract. A useful answer recognizes when not to use recursive type programming.

The type-design capstone combines finite keys and correlated payloads in a small event registry. It intentionally avoids recursion so the practice focuses on a real application relationship rather than compiler endurance.

Optional stretch — after the core task works: Introduce a bounded depth parameter and measure checker cost.

Use recursive transformations only with an explicit domain and practical limits.