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.
| Tool | Scope | Depth | Use when |
|---|---|---|---|
readonly prop | One property | Shallow | A single field is immutable |
Readonly<T> | All properties | Shallow | The whole object is immutable |
as const | Whole literal value | Deep | Locking a literal’s exact shape |
ReadonlyArray<T> | Array | Shallow | A 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.
readonlyis erased during compilation — it exists only in the type system. For guarantees that survive at runtime, callObject.freeze(), but note it is also shallow.
Best Practices
- Mark identifiers, keys, and timestamps
readonlyso they cannot be reassigned by mistake. - Use
Readonly<T>to lock a whole shape; use the modifier when only some fields are frozen. - Remember
readonlyis 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 constto deeply freeze literal values and narrow them to literal types. - Pair
const(no rebinding) withreadonly(no property writes) — they cover different cases. - Use
Object.freeze()when you need an immutability guarantee that persists at runtime.