Q38 of 40 · JavaScript
What does `Object.freeze()` do, and how does it compare to TypeScript's `readonly`?
Short answer
Short answer: `Object.freeze()` is a runtime enforcement — frozen object properties cannot be added, deleted, or modified. It is shallow: nested objects are not frozen. TypeScript `readonly` is compile-time only — the JS output has no freeze call, so mutations are still possible at runtime.
Detail
These two mechanisms both restrict mutations but operate at different levels.
Object.freeze(): A runtime operation that makes an object non-extensible (no new properties), seals it (no deletions), and makes all own properties non-writable. Mutations silently fail in sloppy mode; they throw TypeError in strict mode.
Shallow freeze: Only the top-level is frozen. Properties that are objects still point to mutable references. To deep-freeze, you must recursively freeze all nested objects.
TypeScript readonly: A compile-time constraint. The transpiled JavaScript output contains no Object.freeze call — a readonly property can still be mutated at runtime through non-typed paths (type assertions, any, or runtime-generated values). TypeScript's as const infers deeply readonly literal types but still does not emit freeze.
When to use which:
- Use
Object.freeze()when you need a true runtime constant — configuration objects, lookup tables, test fixtures you want to protect from accidental mutation. - Use TypeScript
readonlyfor compile-time contract enforcement — the common case. - Combine both for maximum safety on critical constants.
// EXAMPLE
// Object.freeze — runtime
const CONFIG = Object.freeze({
baseUrl: "https://api.example.com",
timeout: 5000,
nested: { retries: 3 }, // NOT frozen — only shallow!
});
// "use strict";
// CONFIG.timeout = 9000; // TypeError: Cannot assign to read only property
CONFIG.nested.retries = 10; // silently succeeds — nested not frozen!
// Deep freeze utility
function deepFreeze(obj) {
Object.getOwnPropertyNames(obj).forEach(name => {
const val = obj[name];
if (typeof val === "object" && val !== null) deepFreeze(val);
});
return Object.freeze(obj);
}
// TypeScript readonly — compile-time only
interface Config { readonly timeout: number; }
const cfg: Config = { timeout: 5000 };
// cfg.timeout = 9000; // TypeScript error — but JS runtime allows it
(cfg as any).timeout = 9000; // succeeds at runtime!