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.
What You'll Learn
💡 Real-World Analogy
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 & Readonly<T>
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 ReadonlyArray<T> ) removes mutating methods like push from the type. And the Readonly<T> 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.
Common Errors (and the fix)
- ❌ Expecting readonly to block runtime mutation — it's erased before the code runs. ✅ Use Object.freeze when you need a real runtime guarantee.
- ❌ "Cannot assign to 'x' because it is a read-only property" — you reassigned a readonly member. ✅ Set it at construction, or drop readonly if it must change.
- ❌ Mutating a nested object after Object.freeze — freeze is shallow. ✅ Write a recursive deepFreeze to lock every level.
- ❌ "Property 'push' does not exist on type 'readonly string[]'" — you tried to mutate a readonly array. ✅ Build a new array ( [...arr, x] ) instead of mutating.
Pro Tips
- 💡 Use as const for constant config and lookup tables so values stay literal and can't be edited by accident.
- 💡 Prefer non-mutating array operations ( map , filter , spread) over push / splice when working with readonly arrays.
- 💡 Combine types and freeze — as const for editor safety and Object.freeze for runtime safety on truly critical constants.
- 💡 Reach for Readonly<T> to derive an immutable view of an existing type without rewriting every property by hand.
📚 Keep learning
- TypeScript Handbook — readonly Properties
- TypeScript Handbook — Readonly<Type> Utility
- TypeScript Docs — const assertions (as const)
Frequently Asked Questions
Mini-Challenge: Deep Freeze
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.
🎉 Lesson Complete
- ✅ readonly blocks reassignment in the editor — it's compile-time only
- ✅ as const gives deep readonly plus narrow literal types
- ✅ readonly T[] / ReadonlyArray<T> remove mutating array methods
- ✅ Readonly<T> flips a whole object type to readonly
- ✅ Object.freeze enforces immutability at runtime — but only shallowly
- ✅ Next lesson: Records & Index Signatures — typing dictionaries
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.
Continue this course
- Previous: type vs interface
- Next: Records & Index Signatures