TypeScript Academy · Lesson

Deep Immutability Patterns

Achieve deeply readonly structures with recursive types.

Lesson 4 of 413 steps

Deep Immutability Patterns is a free TypeScript Academy lesson on CoddyKit. This is lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the TypeScript Academy learning path, and your progress syncs across the web and the CoddyKit app. The TypeScript Academy course includes 4 lessons in total.

Shallow vs Deep Immutability

The readonly modifier is shallow: it protects the top level but not nested objects. Deep immutability means every level, all the way down, is readonly. Achieving it takes extra work.

interface State { readonly user: { name: string }; }
const s: State = { user: { name: 'Ada' } };
// s.user = {...}; // Error (shallow protection)
s.user.name = 'Grace'; // allowed! nested field is mutable
console.log(s.user.name);

Why Shallow Isn't Enough

For state management and shared data, shallow readonly leaves nested fields exposed. A single overlooked mutation deep in a structure can cause subtle, hard-to-trace bugs.

interface Config { readonly db: { host: string; port: number }; }
const c: Config = { db: { host: 'localhost', port: 5432 } };
c.db.port = 9999; // mutates nested data despite readonly
console.log(c.db.port);

A Recursive DeepReadonly Type

We can build a mapped type that applies readonly recursively. DeepReadonly<T> walks every property and, if it's an object, recurses into it.

type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? DeepReadonly<T[K]>
    : T[K];
};
console.log('DeepReadonly defined');

Applying DeepReadonly

Wrapping a type in DeepReadonly makes nested properties immutable too. Now even s.user.name cannot be reassigned — the protection reaches all the way down.

type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? DeepReadonly<T[K]>
    : T[K];
};
type State = DeepReadonly<{ user: { name: string } }>;
const s: State = { user: { name: 'Ada' } };
// s.user.name = 'x'; // Error now
console.log(s.user.name);

How the Mapped Type Works

The [K in keyof T] iterates every key. The readonly prefix locks each one. The conditional T[K] extends object ? ... : T[K] decides whether to recurse or stop at a primitive.

type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? DeepReadonly<T[K]>
    : T[K];
};
type Cfg = DeepReadonly<{ a: number; b: { c: string } }>;
const cfg: Cfg = { a: 1, b: { c: 'x' } };
console.log(cfg.a, cfg.b.c);

Compile-Time Only Protection

DeepReadonly enforces immutability in the type system, not at runtime. The JavaScript object is still technically mutable through untyped paths — but your typed code is fully protected.

// Type-level immutability does not freeze the runtime object.
// For runtime guarantees, combine it with Object.freeze.
console.log('Types guard your code; freeze guards runtime');

Freezing Nested Objects at Runtime

For actual runtime immutability, use Object.freeze. It prevents adding, removing, or changing properties — but, like readonly, it's shallow by default.

const config = Object.freeze({ host: 'localhost', port: 8080 });
console.log(config.port);
// In strict mode, config.port = 1 throws; otherwise silently ignored.

A Deep Freeze Helper

To freeze deeply at runtime, recurse through nested objects and freeze each one. This pairs naturally with the type-level DeepReadonly for end-to-end immutability.

function deepFreeze<T>(obj: T): T {
  Object.values(obj as any).forEach((v) => {
    if (v && typeof v === 'object') deepFreeze(v);
  });
  return Object.freeze(obj);
}
const frozen = deepFreeze({ a: { b: 1 } });
console.log(frozen.a.b);

Immutability for Safer State

Deep immutability is the backbone of predictable state. When data can't be mutated in place, updates must create new objects, making changes explicit and easy to track.

const state = { count: 0, user: { name: 'Ada' } };
// Immutable update: create a new object instead of mutating
const next = { ...state, count: state.count + 1 };
console.log(state.count, next.count);

Immutable Updates Pattern

With readonly data you produce new versions via spreading. Each update is a fresh object, so previous states remain intact — invaluable for undo, time-travel debugging, and change detection.

type State = { readonly items: readonly string[] };
const s: State = { items: ['a'] };
const updated: State = { items: [...s.items, 'b'] };
console.log(s.items, updated.items);

Combining Type and Runtime Safety

The strongest approach uses both: DeepReadonly for compile-time guarantees and deepFreeze for runtime enforcement. Together they make accidental mutation nearly impossible.

function freeze<T>(obj: T): T {
  return Object.freeze(obj);
}
const settings = freeze({ theme: 'dark', version: 2 });
console.log(settings.theme, settings.version);

Quick Check

Test your understanding of deep immutability.

Recap: Deep Immutability

You learned that:

  • readonly is shallow; nested fields stay mutable.
  • A recursive DeepReadonly<T> mapped type applies readonly at every level.
  • It's compile-time only; use Object.freeze (deeply) for runtime enforcement.
  • Immutability enables safe, predictable state via copy-on-update patterns.

That completes the TypeScript Academy track on advanced types and immutability.

type State = { readonly value: number };
const s: State = { value: 1 };
const next: State = { ...s, value: s.value + 1 };
console.log(s.value, next.value);
Free to start

Learn TypeScript with an AI tutor — free

Write and run real code in your browser, get instant help from a 24/7 AI tutor, and pick up where you left off on the web or in the app.

Courses
101
Lessons
352

Frequently Asked Questions

Is the “Deep Immutability Patterns” lesson free?

Yes — the full text of “Deep Immutability Patterns” is free to read here on the web. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the TypeScript Academy course, upgrade to CoddyKit PRO. The TypeScript Academy course includes 4 lessons in total.

What will I learn in “Deep Immutability Patterns”?

Achieve deeply readonly structures with recursive types. You practise TypeScript Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start TypeScript Academy?

No prior experience is required. TypeScript Academy on CoddyKit is structured for beginners through advanced learners, so you can start here or from the beginning and move at your own pace. This is lesson 4 of 4.

How long does the “Deep Immutability Patterns” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this TypeScript Academy lesson?

Yes. Every TypeScript Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. readonly Properties
  2. readonly Arrays and Tuples
  3. ReadonlyArray and ReadonlyMap
  4. Deep Immutability Patterns
← Back to TypeScript Academy