Readonly Members
The readonly modifier marks an interface property as immutable: it can be set when the object is created but never reassigned afterward. This communicates intent — “this value is fixed” — and lets the compiler catch accidental mutations before they reach runtime. Like all of TypeScript’s type system, readonly is a compile-time check only; it does not freeze objects at runtime. This page covers declaring readonly members, where the guarantee ends, the difference from const, and the related ReadonlyArray and Readonly<T> tools.
Declaring readonly properties
Prefix a property with readonly to forbid reassignment after construction. You can still assign the initial value in an object literal or constructor.
interface Account {
readonly id: string;
readonly createdAt: Date;
balance: number;
}
const account: Account = {
id: "acc_123",
createdAt: new Date(),
balance: 100,
};
account.balance = 150; // ✅ ok — balance is mutable
account.id = "acc_456"; // ❌ Error: Cannot assign to 'id' because it is a read-only property.
The id and createdAt fields are locked once set, while balance remains freely mutable. This is ideal for identifiers and timestamps that should never change over an entity’s lifetime.
readonly is shallow
The modifier prevents reassigning the property itself, but it does not make the referenced value deeply immutable. You can still mutate the contents of a readonly object or array property.
interface Team {
readonly members: string[];
}
const team: Team = { members: ["Alice"] };
team.members.push("Bob"); // ✅ allowed — the array is mutated, not reassigned
team.members = ["Carol"]; // ❌ Error: Cannot assign to 'members'
To prevent mutation of the array’s elements as well, type the property as a readonly string[] (or ReadonlyArray<string>), which removes mutating methods like push and splice from its type.
interface SafeTeam {
readonly members: readonly string[];
}
readonlyenforcement happens only in TypeScript. At runtime the property is a normal, writable JavaScript property. For true runtime immutability useObject.freeze, which also narrows the type toReadonly<T>.
readonly vs const
const and readonly solve different problems and are not interchangeable. const applies to a variable binding and prevents reassigning the binding. readonly applies to a property and prevents reassigning that property.
const user = { name: "Alice" };
user.name = "Bob"; // ✅ ok — const only locks the binding, not properties
interface Frozen { readonly name: string }
const frozen: Frozen = { name: "Alice" };
frozen.name = "Bob"; // ❌ Error — readonly locks the property
| Feature | const | readonly |
|---|---|---|
| Applies to | Variable bindings | Object properties |
| Prevents | Rebinding the variable | Reassigning the property |
| Affects nested values | No | No (shallow) |
| Runtime effect | Block-scoped binding | None (type-only) |
Readonly via utility types and as const
You rarely need to write readonly on every member by hand. The built-in Readonly<T> utility maps an existing type so all of its properties become readonly, and as const infers deeply readonly literal types.
interface Config {
host: string;
port: number;
}
type FrozenConfig = Readonly<Config>;
// { readonly host: string; readonly port: number }
const defaults = {
host: "localhost",
port: 8080,
} as const;
// type: { readonly host: "localhost"; readonly port: 8080 }
Readonly<Config> is perfect for function parameters that should not be mutated, while as const is ideal for fixed configuration literals where you also want the narrow literal types preserved.
Readonly arrays and tuples
Arrays and tuples have dedicated readonly forms that strip out mutating operations, which is the safest way to accept a collection you promise not to change.
function sum(values: readonly number[]): number {
// values.push(0); // ❌ Error: push does not exist on readonly number[]
return values.reduce((total, n) => total + n, 0);
}
const scores: readonly number[] = [10, 20, 30];
sum(scores); // ✅ ok
Accepting readonly number[] is strictly more permissive for callers — both mutable and readonly arrays are assignable to it — while guaranteeing your function will not modify the input.
Best Practices
- Mark identifiers, timestamps, and configuration fields
readonlyto document immutability. - Remember
readonlyis shallow; usereadonly T[]to lock array contents too. - Use
readonlyfor properties andconstfor variable bindings — they are not the same. - Accept
readonlyarrays in functions that must not mutate their inputs. - Reach for
Readonly<T>to derive an immutable version of an existing interface. - Use
as constfor fixed literal objects to get deep readonly plus narrow types. - For runtime guarantees, combine the type with
Object.freeze.