skip to content

In TypeScript, what does marking a class property `readonly` guarantee, and how is that different from declaring a variable with `const`?

level: juniorimportance: must knowfreq 72%

answer

  1. one restricts a property, one a binding
  2. two legal write sites
  3. declaration or constructor only
  4. shallow — contents stay mutable
  5. erased at compile time

basics

~20 s

readonly is a compile-time-only restriction on writing a property: assignment is allowed just in the property's declaration or the declaring class's constructor. const restricts rebinding a variable instead. Neither stops mutation of the object inside.

solid answer

~50 s

`readonly` is a TypeScript modifier on a **property**. The checker permits an assignment to it in exactly two places: the property's own initializer, and the constructor of the class that declares it. Anywhere else — a method, a subclass constructor, or code outside the class — you get "Cannot assign to 'x' because it is a read-only property". `const` is a JavaScript **declaration form** for a variable binding: it says the binding can never be re-pointed, and it says nothing about properties. The two also differ in lifetime: `const` survives into the emitted JavaScript and is enforced by the engine, while `readonly` is erased with the rest of the type layer, so the compiled output has no check at all. Both are shallow — `readonly items: string[]` still lets you `push` into the array, just as `const obj = {}` still lets you assign `obj.a`.

go deeper

for a junior

Be able to say plainly that readonly goes on a property and const goes on a variable, and that a readonly field can be set in its declaration or the constructor and nowhere else.

for a middle

Explain that readonly is erased at compile time while const is real JavaScript the engine enforces, and show that both are shallow by mutating the contents of a readonly array field.

for a senior

Show where the guarantee stops mattering in production code — untyped consumers, assertion boundaries, aliasing through a mutable-typed variable — and say when you would spend a runtime mechanism instead.

for a principal

Own the convention: decide whether the codebase treats readonly as the default for constructor-set fields, what that buys in review, and where a team pays for confusing a checked promise with an enforced one.

## Two different things are being restricted The confusion between `const` and `readonly` disappears once you see that they constrain different things. `const` is a JavaScript declaration form. It creates a *binding* — a name — and forbids that name from ever being pointed at a different value. It has nothing to say about the value itself. `readonly` is a TypeScript-only modifier that goes on a *property* of a class (or of an object type). It tells the type checker to reject writes to that property outside a small allowed window. It cannot be applied to a variable declaration, and `const` cannot be applied to a class member — they are not two spellings of one idea, they are two different tools. ```ts const limit = 10; limit = 11; // Error: Cannot assign to 'limit' because it is a constant class Config { readonly url: string; constructor(url: string) { this.url = url; } } ``` ## Where the compiler allows a write A `readonly` property may be assigned in exactly two positions: 1. Its own declaration initializer. 2. The body of the constructor of the class that declares it. Everything else is an error, including a method on the same class and, importantly, the constructor of a subclass — the privilege belongs to the declaring class, not to the inheritance chain. ```ts class Account { readonly id: string; readonly createdAt = new Date(); // allowed: declaration initializer constructor(id: string) { this.id = id; // allowed: declaring class's constructor } rename(id: string) { this.id = id; // Error: read-only property } } ``` That constructor window is the whole point of the feature: it lets you model a value that is decided once, at construction, and never again. It is the type-level way of saying "this is part of the object's identity". ## Both are shallow Neither modifier reaches inside the value. ```ts class Basket { readonly items: string[] = []; } const b = new Basket(); b.items.push("apple"); // fine — the array itself is not readonly b.items = []; // Error — the property binding is ``` The same is true of `const`: `const cfg = { retries: 3 }` happily accepts `cfg.retries = 5`. If you want the elements protected too, you have to say so in the element type (a `readonly` array type, or a mapped-over type) — the modifier on the outer property does not propagate. ## readonly is erased; const is not This is the point interviewers usually push on. TypeScript emits JavaScript with the type layer removed, and `readonly` is part of that layer. The compiled class has an ordinary writable property; nothing in the runtime object stops a write. `const`, by contrast, is real JavaScript syntax that the engine enforces — reassigning a `const` throws a `TypeError` at run time. So `readonly` protects *your* code while it is being checked. It does not protect an object handed to untyped JavaScript, to a consumer who used a type assertion, or to anything crossing an `any` boundary. ## static readonly `static readonly` marks a class-level constant: it lives on the constructor object rather than on instances, and it may be assigned in its initializer or in a `static` initialization block. It is a common way to give a constant a namespace — `Http.DEFAULT_TIMEOUT` — where a module-level `const` would be a bare exported name. The `readonly` half is still erased; only the `static` member itself survives into the emitted JavaScript. ## Choosing between them Use `const` for every local and module-level binding by default — it is free and enforced. Use `readonly` on class fields that are set at construction and should never change afterwards: identifiers, injected collaborators, configuration snapshots. It costs nothing at run time and turns a class of bugs into compile errors, which is exactly the trade the type layer exists to make. What it does *not* buy you is immutability. If a caller must be unable to mutate the value even from untyped code, that is a runtime concern and needs a runtime mechanism — a private field with only a getter, or freezing the object — not a type modifier.

  • Can a subclass constructor assign to a `readonly` property declared on its base class?
    No. The write privilege belongs to the class that declares the property, so a derived constructor assigning `this.id` gets the read-only error even though the field is inherited and visible. If a subclass needs to decide the value, it passes it up through `super(...)` and lets the base constructor do the assignment.
  • If `readonly` is erased, can another piece of typed code still write the property?
    Yes. TypeScript ignores `readonly` differences when checking assignability between object types, so you can assign the instance to a variable typed with an identical but mutable shape and write through that alias with no error. It is a known, deliberate hole in the checker rather than a bug.
  • When would you reach for `static readonly` instead of an exported module-level `const`?
    When the constant is conceptually part of the class's public surface and reads better qualified — `Retry.MAX_ATTEMPTS` rather than a loose imported name. Mechanically the module `const` is stronger, since the engine enforces it; `static readonly` only enforces at check time, but it groups related constants with the type that owns them.

saying these in an interview costs you the question

  • Says readonly makes the object immutable at run time
  • Treats readonly and const as interchangeable spellings
  • Claims readonly compiles to Object.freeze
  • Thinks readonly can be assigned in any method of the class
  • Believes readonly on an array property blocks push

context