Readonly Properties in TypeScript
Readonly marks TypeScript object properties as immutable at compile time. Add the modifier before a property name, and TypeScript prevents reassignment. Learn the syntax and its limits.
TypeScript readonly properties cannot be reassigned after the object is created. You mark one with the readonly modifier before the property name. TypeScript enforces this at compile time, but the restriction does not exist at runtime.
Use readonly when you want to make it clear that a property should be set once and never changed. It is a signal of intent, backed by the compiler.
Marking a property as readonly
Add the readonly keyword before the property name in any object type. After creation, trying to change a readonly property produces a compile error:
interface Point {
readonly x: number;
readonly y: number;
}
const origin: Point = { x: 0, y: 0 };
origin.x = 5;
// Error: Cannot assign to 'x' because it is a read-only property.You can still read readonly properties freely. The restriction only applies to assignment on the left side of an equals sign.
Readonly vs const
The rule is simple: use const for variables, readonly for properties.
| Modifier | Where used | What it prevents |
|---|---|---|
| const | Variable declarations | Reassigning the variable to a new value |
| readonly | Object properties | Reassigning the property on an existing object |
A variable declared with const cannot be pointed at a new value. A property marked readonly cannot be reassigned once the object exists. They work at different levels but serve the same purpose: catching unintended mutations.
Readonly is not deep
The readonly modifier only protects the property it is directly on. If that property holds an object, the nested object's own properties are still mutable.
interface Home {
readonly resident: { name: string; age: number };
}
const home: Home = { resident: { name: "Ada", age: 30 } };
home.resident.age += 1; // OK -- nested properties are not readonlyThe age property is not marked readonly, so updating it is allowed. But you cannot replace the entire resident object with a new one, because the resident property itself is readonly.
This is a frequent source of confusion. Readonly is shallow by design. If you need deep immutability, you must mark every nested property as readonly, or build a deep-readonly mapped type. See mapped types in TypeScript for more on that technique.
Readonly arrays
Arrays have a special readonly variant. The ReadonlyArray type removes all mutating methods so the compiler rejects them.
function sum(values: readonly number[]) {
let total = 0;
for (const v of values) total += v;
return total;
}
console.log(sum([1, 2, 3])); // 6This prints 6. You can pass a regular mutable array to a function that expects a readonly array. This is safe because the function promises not to mutate. The reverse is not allowed: you cannot pass a readonly array to a function that expects a mutable one, because that function might call push or splice on it.
Unlike Array, there is no ReadonlyArray constructor. Create a regular array and assign it to a readonly-typed variable instead.
Readonly and type compatibility
TypeScript does not consider readonly when checking whether two types are compatible. A readonly object can be assigned to a mutable variable of the same shape.
interface Person { name: string; age: number; }
interface ReadonlyPerson { readonly name: string; readonly age: number; }
let writable: Person = { name: "Ada", age: 30 };
let ro: ReadonlyPerson = writable;
writable.age += 1;
console.log(ro.age); // 31The ro variable is typed as ReadonlyPerson, so you cannot assign to its properties directly. But the same object is also referenced by writable, which can mutate it. The readonly view does not prevent the underlying object from changing through a different reference. This means readonly is a best-effort compile-time guard, not a security guarantee.
When to use readonly
Use readonly when a property should be set during creation and never changed -- like an ID or a creation timestamp. Also use it for function parameters that should not be mutated by the function body, especially arrays. For related patterns, see readonly values in TypeScript.
Do not use readonly when the object is meant to change freely or when you need runtime enforcement of immutability. For runtime enforcement, use Object.freeze.
Common mistakes
Expecting readonly to be deep. Marking a property readonly only protects reassignment of that exact property. Nested objects inside it are still mutable. Repeat readonly at every level you want to protect.
Expecting readonly to prevent mutation at runtime. Readonly is erased. If another piece of code has a mutable reference to the same object, it can change the value. Readonly is a development-time signal, not a runtime lock.
Using const where readonly belongs, or vice versa. Variables get const. Properties get readonly. There is no const for object properties and no readonly for standalone variables.
Rune AI
Key Insights
- Add readonly before a property name to prevent reassignment at compile time.
- Readonly is not deep: nested objects inside a readonly property can still be mutated.
- Use readonly on properties and const on variables -- they serve similar roles at different levels.
- ReadonlyArray prevents mutating array methods but still lets you read elements.
- Readonly objects can be assigned to mutable variables; readonly does not affect type compatibility.
Frequently Asked Questions
Does readonly prevent mutation at runtime?
Can I use readonly on arrays?
How do I remove readonly from a type?
Conclusion
Readonly marks a property as immutable at compile time. It does not change runtime behavior and does not make nested objects deeply immutable. Use it to signal intent: this property is set once and should not change. Think of it as documentation the compiler can enforce.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.