"Compile-time immutability. The runtime doesn't know, the compiler refuses to let you forget."
What readonly does
readonly is a property modifier that prevents writes after construction. interface Box { readonly width: number } means width is read-only — assigning to it after the object exists is a compile error. The property can be read normally; reassignment fails.
This is a compile-time contract. At runtime, the property is a regular property, fully writable. The type system enforces the rule; the JavaScript runtime doesn't know it exists. This is consistent with the two-layer mental model from the Foundations track: types are erased.
Where readonly goes
- Object property:
{ readonly id: number; name: string }—idis locked,nameis mutable. - Interface property:
interface User { readonly id: number; name: string }— same rule, named declaration. - Class field:
class Box { readonly width: number; constructor(w: number) { this.width = w } }— the constructor can write, nothing else can. - Array type:
readonly number[]orReadonlyArray<number>— push/pop/sort/etc are removed from the type. - Tuple type:
readonly [number, string]— same idea, position-aware.
Why compile-time-only is enough
The pragmatic answer: most mutation bugs happen in code you control. If your team has agreed not to write to readonly properties, the compiler can enforce that agreement. The runtime would only need to enforce it against malicious code or genuinely external mutation — and those cases are rare in front-end app code.
The deeper answer: making readonly runtime-enforced would require either freezing objects (slow, deep) or wrapping in proxies (slow, intrusive). The TypeScript designers chose the trade: zero runtime cost, real compile-time safety. If you need true runtime immutability, reach for Object.freeze() or a library like Immer.
readonly with as const for the strongest compile-time guarantees on literals. const config = { port: 5173 } as const makes every property readonly and narrows every value to its literal type. The combination is what teams reach for when they want "can't change this, the compiler should refuse."readonly is shallow by default
readonly { nested: { name: string } } means the outer reference is readonly, but nested.name is still writable. The compiler does not recurse. If you want deep readonly, you can build it with mapped types (covered in the Type Manipulation track), or use the standard Readonly<T> utility — which is also shallow. Deep immutability is a recursive operation you opt into.
Pippa's confession
readonly heavily on configuration shapes and on props that get passed to deeply nested React components. The compiler refuses to let a child mutate the parent's data. The runtime still sees plain JavaScript — but in practice, the contract has caught enough "oops I mutated a prop" mistakes that the rule earns its keep on every refactor.