readonly & as const
readonly and as const are compile-time guarantees that block reassignment in your editor, while Object.freeze is the runtime tool that actually prevents mutation when the code runs.
Learn readonly & as const in our free TypeScript course — an interactive lesson with runnable examples, a practice exercise and a quick reference.
Part of the free TypeScript course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
Imagine a museum display. A "Do Not Touch" sign ( readonly ) tells visitors not to handle the exhibit — but a determined person could still reach in; it's a rule, not a barrier. A glass case ( Object.freeze ) physically prevents touching at runtime. And the glass case only covers what's inside it — items on a shelf behind the case (nested objects) are still reachable unless you case those too (deep freeze).
1. readonly Properties
Mark a property readonly and TypeScript lets you set it when the object is created but rejects any later reassignment. It's perfect for identifiers and configuration that shouldn't change after construction. The crucial caveat: this is a compile-time guarantee only. It's erased before the code runs, so the JavaScript engine itself imposes no restriction.
2. as const , ReadonlyArray &
Three related tools build on readonly . as const applied to a literal makes it deeply readonly and pins each value to its narrowest literal type. A readonly T[] (or ) removes mutating methods like push from the type. And the utility flips every property of an existing object type to readonly in one step.
3. Compile-time readonly vs Runtime Object.freeze
These two solve different problems. readonly and as const catch mistakes in your editor and vanish at runtime. Object.freeze is real, executing code that actually blocks mutation when the program runs (throwing in strict mode, silently ignoring otherwise). But freeze is shallow : it locks only the top level, leaving nested objects mutable. For full safety you often want both — types for the editor, freeze for the runtime.
🎯 Your Turn
Make an object truly immutable at runtime. Fill in the blank marked ___ with the right method, then run it.
No blanks this time — just a brief and a starting outline. Build the recursive freeze yourself, run it, and check your output against the example in the comments.
Practice quiz
When does 'readonly' actually prevent a property from changing?
- At runtime, always
- Only inside strict mode at runtime
- At compile time only - it is erased before the code runs
- Never, it is just a comment
Answer: At compile time only - it is erased before the code runs. readonly is a type-level guarantee; the JS engine imposes no restriction at runtime.
What does 'as const' do to a literal value?
- Makes it deeply readonly and pins narrow literal types
- Freezes it at runtime
- Converts it to a string
- Adds a runtime guard
Answer: Makes it deeply readonly and pins narrow literal types. as const infers the narrowest literal types and makes the whole value deeply readonly in the types.
Can an interface mix readonly and mutable members?
- No, all members must match
- Only if you use as const
- Only in classes
- Yes - e.g. readonly id with a mutable count
Answer: Yes - e.g. readonly id with a mutable count. interface Counter { readonly id: string; count: number } is valid; count can change, id cannot.
Which tool actually blocks mutation when the program runs?
- readonly
- Object.freeze
- as const
- the Readonly<T> utility
Answer: Object.freeze. Object.freeze is real runtime code; readonly and as const are type-only.
Why is Object.freeze described as shallow?
- It freezes only top-level properties, leaving nested objects mutable
- It only freezes arrays
- It freezes nothing
- It only works once per object
Answer: It freezes only top-level properties, leaving nested objects mutable. Nested objects stay mutable unless you recursively deep-freeze them.
What happens when you call push on a 'readonly string[]'?
- It works fine
- It throws at runtime
- TypeScript errors: 'push' does not exist on a readonly array
- It returns undefined
Answer: TypeScript errors: 'push' does not exist on a readonly array. Readonly arrays remove mutating methods like push from the type.
What does the Readonly<T> utility type do?
- Freezes the object at runtime
- Flips every property of T to readonly
- Removes optional properties
- Makes T deeply immutable including nested
Answer: Flips every property of T to readonly. Readonly<T> maps every top-level property of T to readonly (shallow).
In non-strict mode, assigning to a frozen object's property does what?
- Throws a TypeError
- Updates the value
- Deletes the property
- Silently fails with no change
Answer: Silently fails with no change. In non-strict mode the assignment silently fails; in strict mode it throws.
How do you confirm an object is frozen at runtime?
- typeof obj === 'frozen'
- Object.isFrozen(obj)
- obj.frozen
- Object.readonly(obj)
Answer: Object.isFrozen(obj). Object.isFrozen returns true for a frozen object.
For deep runtime immutability of nested objects you should:
- Use readonly everywhere
- Call Object.freeze once
- Recursively freeze nested objects before freezing the parent
- Use as const
Answer: Recursively freeze nested objects before freezing the parent. A recursive deepFreeze walks into each nested object before freezing the parent.