Skip to content
TypeScript ts objects 4 min read

Readonly Properties

Immutability prevents a whole class of bugs where code mutates an object another part of the program still depends on. TypeScript expresses this at compile time with the readonly modifier, which forbids reassigning a property after the object is created. It is a purely static guarantee — it disappears at runtime — but it is enough to catch accidental writes during development. This page covers per-property readonly, the Readonly<T> utility, how shallow these guarantees are, and how as const relates.

The readonly modifier

Prefix a property with readonly to block assignment to it after initialization. You can still read the property freely; only writing is rejected.

interface Account {
  readonly id: string;
  balance: number;
}

const acc: Account = { id: "ac_1", balance: 100 };

acc.balance = 150; // ✅ balance is mutable

// ❌ Error: Cannot assign to 'id' because it is a read-only property.
acc.id = "ac_2";

The property is set once when the object literal is created (or in a class constructor) and is locked thereafter. This is ideal for identifiers, timestamps, and other fields that should never change once assigned.

Readonly<T>

To make every property of a type read-only without rewriting it, wrap it in the built-in Readonly<T> utility. It is a mapped type that adds readonly to each member.

interface Point {
  x: number;
  y: number;
}

const origin: Readonly<Point> = { x: 0, y: 0 };

// ❌ Error: Cannot assign to 'x' because it is a read-only property.
origin.x = 10;

Readonly<Point> is equivalent to { readonly x: number; readonly y: number }. Use the per-property modifier when only some fields are immutable, and Readonly<T> when the whole object should be frozen. See Partial, Required & Readonly for the utility family.

ToolScopeDepthUse when
readonly propOne propertyShallowA single field is immutable
Readonly<T>All propertiesShallowThe whole object is immutable
as constWhole literal valueDeepLocking a literal’s exact shape
ReadonlyArray<T>ArrayShallowA list should not be mutated

readonly is shallow

The most important caveat: readonly only protects the property binding, not the object it points to. A readonly property holding an object still lets you mutate that nested object’s fields.

interface Config {
  readonly server: { host: string; port: number };
}

const config: Config = { server: { host: "localhost", port: 80 } };

config.server.port = 443; // ✅ allowed — nested object is still mutable

// ❌ Error: Cannot assign to 'server' because it is a read-only property.
config.server = { host: "x", port: 1 };

You cannot replace config.server, but you can edit its port. For deep immutability you need a recursive DeepReadonly mapped type or runtime freezing. The built-ins are all shallow.

Readonly arrays and tuples

Arrays have their own read-only form. ReadonlyArray<T> (or the shorthand readonly T[]) removes mutating methods like push and splice and forbids index assignment.

function sum(nums: readonly number[]): number {
  // nums.push(0); // ❌ Error: 'push' does not exist on 'readonly number[]'
  return nums.reduce((a, b) => a + b, 0);
}

const scores: readonly number[] = [1, 2, 3];
// ❌ Error: Index signature in type 'readonly number[]' only permits reading.
scores[0] = 9;

Accepting readonly number[] in a function signature is a strong way to promise the caller you will not mutate their array. Note a readonly T[] is not assignable to a mutable T[], which prevents leaking write access.

readonly vs const

const and readonly solve different problems. const is a runtime keyword that prevents rebinding a variable; readonly is a compile-time modifier that prevents reassigning a property. You often use both.

const user = { name: "Eve" }; // const: cannot rebind `user`
user.name = "Eva";            // ✅ but properties are still mutable

const frozen = { name: "Eve" } as const; // as const: deep readonly literal
// ❌ Error: Cannot assign to 'name' because it is a read-only property.
frozen.name = "Eva";

const alone does nothing for property mutation. To lock the contents of a literal, add as const, which makes every property readonly and narrows values to their literal types.

readonly is erased during compilation — it exists only in the type system. For guarantees that survive at runtime, call Object.freeze(), but note it is also shallow.

Best Practices

  • Mark identifiers, keys, and timestamps readonly so they cannot be reassigned by mistake.
  • Use Readonly<T> to lock a whole shape; use the modifier when only some fields are frozen.
  • Remember readonly is shallow — nested objects and array contents stay mutable.
  • Accept readonly T[] in functions to promise callers you will not mutate their arrays.
  • Reach for as const to deeply freeze literal values and narrow them to literal types.
  • Pair const (no rebinding) with readonly (no property writes) — they cover different cases.
  • Use Object.freeze() when you need an immutability guarantee that persists at runtime.
Last updated June 29, 2026
Was this helpful?