Deep Immutability Patterns
Achieve deeply readonly structures with recursive types.
Deep Immutability Patterns is a free TypeScript Academy lesson on CoddyKit — 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, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
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:
readonlyis 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);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, and the TypeScript Academy course includes 4 lessons in total. 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.
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; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.
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
- readonly Properties
- readonly Arrays and Tuples
- ReadonlyArray and ReadonlyMap
- Deep Immutability Patterns