TypeScript中是否建议使用Object.freeze?还有哪些对象不可变实现方式?
Object.freeze() for Immutability in TypeScript? Great question! Let’s break this down. Object.freeze() is a valid tool for immutability, but it has caveats you need to be aware of:
- It’s shallow by default: If your object has nested objects or arrays, those inner values can still be modified. For example:
const frozenObj = Object.freeze({ nested: { count: 1 } }); frozenObj.nested.count = 2; // This works at runtime (and TypeScript won’t flag it by default!) - TypeScript doesn’t automatically enforce read-only types: To get compile-time checks, pair it with the
Readonly<T>utility type:type MyObj = { value: number }; const frozen: Readonly<MyObj> = Object.freeze({ value: 10 }); frozen.value = 20; // TypeScript throws an error here - Performance impact is minimal for simple objects: For most everyday use cases, the overhead of freezing is negligible. But for deeply nested or frequently created objects, it might add up.
So, to answer the core question: Object.freeze() is a good choice for flat, shallow objects where you want runtime immutability, but it’s not sufficient for nested structures without extra work.
If Object.freeze() doesn’t meet your needs, here are the most common alternatives:
1. TypeScript’s Readonly<T> Utility Type
This is a compile-only safeguard—it doesn’t affect runtime behavior, but it stops you from accidentally modifying properties during development:
type User = { name: string; age: number }; const user: Readonly<User> = { name: "Miroslav", age: 35 }; user.age = 36; // TypeScript error: Cannot assign to 'age' because it is a read-only property
- Pros: Lightweight, no runtime overhead, integrates seamlessly with TypeScript.
- Cons: Doesn’t prevent runtime modifications (e.g., using type assertions or bypassing TypeScript).
2. Custom Deep Freeze Function
To fix Object.freeze()’s shallow limitation, you can write a recursive deep freeze function that freezes all nested objects:
function deepFreeze<T>(obj: T): Readonly<T> { // Freeze the top-level object Object.freeze(obj); // Recursively freeze nested objects/arrays Object.values(obj).forEach(value => { if (typeof value === "object" && value !== null && !Object.isFrozen(value)) { deepFreeze(value); } }); return obj; } const deepFrozen = deepFreeze({ nested: { count: 1 } }); deepFrozen.nested.count = 2; // Throws an error in strict mode, and TypeScript flags it
- Pros: Full runtime immutability for nested structures.
- Cons: Higher performance cost for complex objects; frozen objects can’t be modified at all, which reduces flexibility.
3. Immutability Libraries
For production-grade immutability, especially with nested structures, dedicated libraries are a great option:
Immer
Immer lets you write "mutating" code that actually produces a new immutable object under the hood—super intuitive:
import { produce } from "immer"; const baseState = { user: { name: "Alice", age: 30 } }; const newState = produce(baseState, draft => { draft.user.age = 31; // Looks like we're modifying, but Immer creates a copy }); console.log(baseState.user.age); // 30 (original remains unchanged) console.log(newState.user.age); // 31 (new object with updated value)
- Pros: Clean, familiar syntax; efficient structural sharing; handles nested structures effortlessly.
- Cons: Adds a third-party dependency (though it’s lightweight).
Immutable.js
This library provides immutable data structures like Map and List that never modify the original instance—any change returns a new copy:
import { Map } from "immutable"; const user = Map({ name: "Bob", age: 25 }); const updatedUser = user.set("age", 26); console.log(user.get("age")); // 25 console.log(updatedUser.get("age")); // 26
- Pros: Battle-tested, full immutability, optimized for performance with large datasets.
- Cons: Requires learning a new API; converting between native objects and Immutable.js structures adds extra steps.
4. Spread Operator & Object.assign (Shallow Immutability)
For simple shallow updates, use native JavaScript syntax to create a new object instead of modifying the original:
const original = { a: 1, b: 2 }; const updated = { ...original, b: 3 }; // Copy all properties, then override b console.log(original.b); // 2 console.log(updated.b); // 3
- Pros: No dependencies, uses native syntax, easy to implement for flat objects.
- Cons: For nested structures, you have to recursively spread each level, which gets messy quickly.
- Use
Object.freeze()+Readonly<T>for flat objects needing runtime + compile-time immutability. - Use
Readonly<T>alone for compile-time safety without runtime restrictions. - Use a deep freeze function or Immer/Immutable.js for nested structures.
- Use spread/
Object.assignfor simple shallow updates.
内容的提问来源于stack exchange,提问作者Miroslav Jonas

