readonly
Blocks reassignment of a property, index signature, array or tuple at compile time only.
interface T {
readonly prop: Type;
readonly [key: string]: Type; // readonly index signature
}
class C {
readonly field: Type = init; // assignable in the constructor only
constructor(readonly param: Type) {}
}
function f(a: readonly Type[], t: readonly [Type, Type]): void {}
type Mutable<T> = { -readonly [K in keyof T]: T[K] }; // strip it in a mapped type- Added in TypeScript 2.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$propertyoptional | modifier | Disallows assignment to the property after initialization. On a class field, assignment is still permitted inside the constructor. |
$index signatureoptional | modifier | Disallows assignment through the index signature, so obj[key] = value is an error. |
$readonly T[] / ReadonlyArray<T>optional | type modifier | An array type with the mutating methods (push, pop, splice, sort) removed and index assignment disallowed. Shorthand added in 3.4. |
$readonly [A, B]optional | type modifier | A tuple whose elements cannot be assigned. This is the type inferred for an array literal with an `as const` assertion. |
$-readonly / +readonlyoptional | mapping modifier= +readonly | Inside a mapped type, removes or adds the modifier. A bare `readonly` implies `+readonly`. |
Errors
Errors at compile time with 'Cannot assign to X because it is a read-only property', and when a readonly array or tuple is assigned to a mutable one. It never throws at runtime.
Changelog
- PHP 3.4The `readonly T[]` and `readonly [A, B]` shorthands were added, along with `as const` assertions which infer readonly tuple and object types.
- PHP 3.4The readonly mapping modifier now converts array and tuple types to their readonly counterparts, so Readonly<number[]> gives readonly number[] instead of a broken mapped object type.
readonly stops the compiler from accepting an assignment. That is the whole feature. It does not freeze anything, and it does not make a value immutable.
Three limits worth internalising
- It is shallow.
readonly resident: { name: string; age: number }forbids replacingresidentbut permitshome.resident.age++. - It is not enforced through aliases. TypeScript does not consider
readonlywhen checking assignability between object types, so a mutable object can be assigned to a readonly-typed variable and then mutated through the original reference. The readonly view changes underneath you. - Nothing exists at runtime. The modifier is erased.
Object.isFrozenreturnsfalse, and any JavaScript caller writes freely. UseObject.freezeif you need an actual runtime guarantee.
Where it pays off
The best return is on function parameters: function sum(xs: readonly number[]) documents and enforces that the callee will not sort or push in place, which is a genuinely common bug. Note the deliberate asymmetry — a string[] is assignable to readonly string[], but not the reverse, so accepting readonly arrays costs callers nothing while returning one constrains them.
Example
When to use it
- Accept an array parameter you promise not to sort or splice in place.
- Lock an entity's id so it can only be set in the constructor.
- Freeze a configuration object's shape at the type level for the whole app.
- Strip readonly from a frozen type with a mapped type when building an editable draft.
Frequently asked questions
Does readonly make an object immutable at runtime in TypeScript?
No — readonly is erased at compile time and provides no runtime protection whatsoever, so Object.isFrozen returns false and plain JavaScript can write to the property. Use Object.freeze if you need enforcement at runtime, and remember that freeze is also shallow.
Why can I still modify a readonly property through another variable?
Because TypeScript ignores readonly when checking assignability between object types, so a mutable object can be assigned to a readonly-typed variable while the original reference stays writable. Mutating through that original reference changes what the readonly view reports, and this is documented handbook behaviour rather than a bug.
What is the difference between readonly and const?
`const` applies to a variable binding and stops you reassigning the variable itself, while `readonly` applies to a property, index signature, array or tuple and stops assignment to that member. A const object's properties are still writable unless they are also declared readonly.
More examples
readonly arrays as a parameter contract
Marking the parameter readonly removed sort from its type, which forced the copy on the next line and left the caller's array in its original order — exactly the in-place-sort bug this catches. The final lines show the one-way assignability: passing a normal array where a readonly one is expected is always fine, so adding readonly to your parameters never breaks callers.
// The callee promises not to mutate.
function topTwo(scores: readonly number[]): number[] {
// scores.sort(); // Property 'sort' does not exist on
// // type 'readonly number[]'.
return [...scores].sort((a, b) => b - a).slice(0, 2);
}
const scores = [7, 2, 9, 4];
console.log(topTwo(scores)); // [ 9, 7 ]
console.log(scores); // [ 7, 2, 9, 4 ] — untouched
// Assignability runs one way only:
let ro: readonly string[] = [];
let mu: string[] = [];
ro = mu; // ok
// mu = ro; // The type 'readonly string[]' is 'readonly' and cannot be
// // assigned to the mutable type 'string[]'.Class fields, as const, and stripping readonly
Three related behaviours. A readonly class field may be assigned in the constructor but nowhere else. `as const` produces a readonly tuple, which is why passing it to a function expecting a mutable tuple is rejected — a frequent surprise. And the -readonly mapping modifier removes the flag, giving the editable counterpart of a frozen type.
class Greeter {
readonly name: string;
constructor(otherName?: string) {
this.name = otherName ?? "world"; // constructor assignment is allowed
}
// err() { this.name = "nope"; } // Cannot assign to 'name'
}
const point = [3, 4] as const; // readonly [3, 4]
// function dist([x, y]: [number, number]) { ... }
// dist(point); // readonly [3, 4] is not assignable to [number, number]
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
type Editable = Mutable<{ readonly a: number; readonly b: string }>;
const e: Editable = { a: 1, b: "x" };
e.a = 2; // fine, readonly was stripped
console.log(new Greeter("Ada").name, point.join(","), e.a);
// Ada 3,4 2
Discussion