Q10 of 38 · TypeScript

How does the `readonly` modifier work in TypeScript, and what are its limitations?

TypeScriptJuniortypescriptreadonlyimmutabilitytypescompile-time

Short answer

Short answer: `readonly` prevents reassignment of a property after initialization at compile time. It does not deep-freeze nested objects — a `readonly` reference to an object still allows mutating that object's properties. It is a compile-time constraint only; the emitted JavaScript contains no runtime enforcement.

Detail

readonly is a TypeScript-only compile-time annotation that prevents reassignment of a property or array element.

On interface/type properties: readonly id: number means the id field cannot be assigned after object creation.

On class properties: readonly class properties can be set in the constructor but nowhere else. TypeScript enforces this at compile time.

Readonly<T> utility type: Wraps an entire type making all properties readonly.

readonly arrays: readonly string[] (or ReadonlyArray<string>) prevents push/pop/splice and reassignment of elements. The as const assertion creates a deeply readonly tuple/literal.

Shallow limitation: Like Object.freeze(), readonly is shallow. A readonly property that holds an object reference does not prevent mutation of that object's own properties — only reassignment of the reference itself.

No runtime impact: The compiled JavaScript output has no enforcement. A non-typed caller or a type assertion (as any) can still mutate readonly properties. Use Object.freeze() for runtime enforcement.

// EXAMPLE

interface User {
  readonly id: number;
  name: string;
}
const user: User = { id: 1, name: "Alice" };
user.name = "Bob";  // ok — name is not readonly
// user.id = 2;     // Error: Cannot assign to 'id' (readonly)

// Readonly utility type
type ImmutableUser = Readonly<User>;

// Class readonly
class Config {
  readonly baseUrl: string;
  constructor(url: string) {
    this.baseUrl = url; // ok in constructor
  }
  setUrl(url: string) {
    // this.baseUrl = url; // Error outside constructor
  }
}

// Shallow — nested object is still mutable!
interface Team {
  readonly members: string[];
}
const team: Team = { members: ["Alice"] };
// team.members = [];         // Error: cannot reassign
team.members.push("Bob"); // OK — push mutates the array, not the reference

// Deep readonly with as const
const LEVELS = ["junior", "mid", "senior"] as const; // ReadonlyArray

// WHAT INTERVIEWERS LOOK FOR

The compile-time-only nature and shallow scope are the key insights. Readonly utility type. The `as const` deep-readonly connection. Knowing `Object.freeze()` is the runtime counterpart.

// COMMON PITFALL

Assuming `readonly` prevents all mutations — it only prevents reference reassignment. The array example (push still works) is a classic gotcha that surprises candidates.