readonly

Blocks reassignment of a property, index signature, array or tuple at compile time only.

Signature
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

ParameterTypeDescription
$propertyoptionalmodifierDisallows assignment to the property after initialization. On a class field, assignment is still permitted inside the constructor.
$index signatureoptionalmodifierDisallows assignment through the index signature, so obj[key] = value is an error.
$readonly T[] / ReadonlyArray<T>optionaltype modifierAn array type with the mutating methods (push, pop, splice, sort) removed and index assignment disallowed. Shorthand added in 3.4.
$readonly [A, B]optionaltype modifierA tuple whose elements cannot be assigned. This is the type inferred for an array literal with an `as const` assertion.
$-readonly / +readonlyoptionalmapping modifier= +readonlyInside 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 replacing resident but permits home.resident.age++.
  • It is not enforced through aliases. TypeScript does not consider readonly when 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.isFrozen returns false, and any JavaScript caller writes freely. Use Object.freeze if 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

Try it yourself
Loading editor…
Press Run to execute the code.

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.

Example · typescript
// 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.

Example · typescript
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

  • Be the first to comment on this lesson.