In TypeScript, what does the `readonly` modifier on an object-type property such as `readonly db: { url: string }` actually prevent, and what does it leave untouched?
answer
- one operation only: assigning the slot
- stops at the first level
- the nested object stays writable
- no descriptor, no freeze, no cost
- a view, not a value guarantee
basics
~20 sreadonly stops the checker from accepting an assignment to that property through that type. It is shallow, so the object the property points at stays fully mutable, and it is erased at compile time, so nothing is protected at runtime.
solid answer
~40 s`readonly` is a compile-time restriction on one operation: assigning to that property through a value of that type. `config.db = other` is an error; everything else still works. It is shallow — `config.db.url = 'x'` compiles fine, because the modifier applies to the property slot, not to the object it references. If the property held an array, `config.items.push(x)` would be allowed too; you would need `readonly items: readonly string[]` to block that. And it is erased: the emitted JavaScript contains no `Object.freeze`, no non-writable descriptor, nothing. So it documents intent and catches accidental writes in checked code, but it is not immutability — a value that arrives through `any`, a type assertion, or plain JavaScript can overwrite the property freely.
go deeper
Know that readonly makes assigning to that property a compile error, and that it does not stop you changing fields inside the object the property points at.
Explain that it applies to the property slot only, that it is shallow so nested objects and arrays stay mutable, and that it is erased so nothing is enforced at runtime.
Show where it is load-bearing — shared config, parameter types that promise not to write — and be candid that it is a checked convention, not immutability, so data that must not change needs a runtime mechanism.
Own the API-surface policy: marking published types readonly costs nothing at runtime but propagates to every consumer's assignability, so decide deliberately how far down your types you push it.
## What the modifier restricts ```ts interface Config { readonly db: { url: string }; readonly retries: number; } declare const config: Config; config.retries = 5; // error: Cannot assign to 'retries' because it is a read-only property. config.db.url = 'other'; // no error ``` The modifier attaches to the *property slot* as seen through this type. Writing to the slot is rejected; doing anything to the value stored in the slot is not the modifier's business. ## Shallow, and deliberately so This is the single most-tested consequence. `readonly` does not recurse. Everything reachable through the property remains mutable: ```ts interface Session { readonly user: { name: string }; readonly tags: string[]; } declare const s: Session; s.user.name = 'mallory'; // ok — the nested object is not readonly s.tags.push('admin'); // ok — the array itself is mutable s.tags = []; // error — this writes the slot ``` To lock the contents you must say so at each level: `readonly user: { readonly name: string }` and `readonly tags: readonly string[]`. There is no flag that makes `readonly` deep. ## `readonly` versus `const` They solve different problems and are not alternatives. `const` fixes a *binding*: the variable cannot be pointed at another value, though the object it holds can be mutated. `readonly` fixes a *property* of a type: any variable holding a value of that type cannot write that property. `const` is a JavaScript declaration with runtime meaning; `readonly` is a type-layer annotation with none. You commonly want both, and they do not overlap. ## It emits nothing After compilation there is no trace. No `Object.defineProperty` with `writable: false`, no `Object.freeze`, no throwing setter. Two consequences follow. First, the guarantee only covers code the checker inspects. A value handed to plain JavaScript, or reached through `any`, or produced by a type assertion, can be written to without complaint: ```ts (config as { retries: number }).retries = 99; // compiles, and mutates ``` Second, `readonly` costs nothing at runtime — there is no property-descriptor lookup, no defensive copy, no allocation. It is free documentation with a checker behind it, which is why marking public API surfaces `readonly` is cheap and worth doing. ## Where it is genuinely load-bearing The modifier earns its keep in three places. In shared configuration or dependency objects, it stops a distant module from reaching in and reassigning a field that everything else has already read. In function parameters, declaring a parameter type with `readonly` properties states that the function will not write through it, which is a real part of the signature's meaning. And in inferred literal types produced by a `const` assertion, `readonly` appears automatically on every property, which is how those types stay stable enough to be used as sources of literal types. ## The parts candidates get wrong The two classic mistakes are believing the modifier is deep and believing it does something at runtime. A third, subtler one: `readonly` is not consulted when the checker decides whether one object type is assignable to another, so the property can be written through an alias of a mutable type. That hole is worth knowing about in its own right, and it is why `readonly` is best described as "a read-only *view*" rather than "an immutable value". ## A working mental model Treat `readonly` as a promise your own code makes to itself, enforced only where types are honoured, only one level deep, and only until someone writes an assertion. That framing gets every downstream question right: use it liberally for intent, use `readonly` recursively when the contents matter, and reach for a runtime mechanism when the data genuinely must not change.
- How is readonly different from const?`const` fixes a variable binding — you cannot rebind the name, but you can mutate the object it holds. `readonly` fixes a property of a type — any holder of that type is blocked from writing that property. `const` is real JavaScript with runtime meaning; `readonly` is erased. They are complementary, not alternatives.
- Given `readonly items: string[]`, can a caller still call items.push?Yes. The modifier only blocks `obj.items = …`; the array object it points at is untouched, so `push`, `splice`, and index assignment all compile. To stop those you must change the element type too — `readonly items: readonly string[]` — which removes the mutating methods from the type.
- How would you actually guarantee the value cannot change at runtime?You cannot do it in the type layer, because the modifier is erased. It takes a runtime mechanism — freezing the object, or handing out defensive copies, or never exposing the mutable reference in the first place. `readonly` catches accidents in checked code; it is not a security or concurrency guarantee.
It is a sign on one door, not a lock on the room: it stops people who read signs, coming through that particular door, and says nothing about what is inside.
saying these in an interview costs you the question
- Says readonly makes the whole object immutable
- Thinks readonly compiles to a non-writable property
- Believes readonly and const are the same modifier
- Claims readonly blocks push on a readonly array property
- Assumes readonly survives into the emitted JavaScript