Skip to content
Theory · Arrays and tuples · lesson 3 of 7

Read-only arrays

A left-luggage office may also have a display case: a row of lockers with glass doors under lock. A visitor sees what lies in each one, can count the items and read the numbers. But they cannot open a door, put an item in or take one out.

An ordinary array is like open lockers. A function that receives an array can change it, and the calling code will not know. Let's look at a function that finds the best score:

TypeScript
function best(list: number[]) {
    list.sort((a, b) => b - a);
    return list[0];
}

const scores = [70, 95, 80];
console.log(best(scores)); // 95
console.log(scores.join(" ")); // 95 80 70

Let's go through the example line by line:

  1. best sorts the array in descending order and returns the first element.
  2. The sort method makes no copy: it rearranges the elements in the same array.
  3. best(scores) prints 95, as expected.
  4. But scores.join(" ") prints 95 80 70: the function rearranged an array it was given only to look at.
  5. a and b in (a, b) => b - a have no type, yet there is no TS7006 error. TypeScript knows that sort on a number[] passes two numbers to this function, so it gives a and b the type number itself.

A bug like this is hard to find: the order breaks in one place and is noticed somewhere else entirely.

The readonly number[] type

The word readonly before an array type makes it a read-only array: its elements can be read, but the array itself cannot be changed.

In the previous theme readonly stood before a property. Before a type it is allowed only on arrays and tuples: readonly User gives error TS1354.

TypeScript
const scores: readonly number[] = [70, 95, 80];
console.log(scores[0]);
scores.push(60); // ✗
TEXT
error TS2339: Property 'push' does not exist on type 'readonly number[]'.

The message means "the type readonly number[] has no property push". The methods that change an array are absent from this type altogether, which is why tsc says push does not exist.

What a read-only array keeps and what it loses:

  • you cannot call push, pop, sort, reverse or splice, anything that changes the array itself;
  • you cannot write by index: scores[0] = 60 gives error TS2542;
  • you can read by index, check length and walk the array with a for…of loop;
  • you can call the methods that return a new array: map, filter, slice, toSorted.

Like an ordinary array, this type has a long form: ReadonlyArray<number> is the same type.

A parameter that promises not to change

Most often readonly is written on a function parameter. Such an annotation is a promise in the function's signature: "I will only read your array". Let's rewrite best:

TypeScript
function best(list: readonly number[]) {
    return list.toSorted((a, b) => b - a)[0];
}

const scores = [70, 95, 80];
console.log(best(scores)); // 95
console.log(scores.join(" ")); // 70 95 80

Let's see what changed:

  1. list: readonly number[] means the array can only be read inside the function.
  2. toSorted returns a sorted copy and leaves the original array alone.
  3. An ordinary array, scores, was passed to best, and that is allowed: the function promises to do less with it than the type number[] permits.
  4. The second line prints 70 95 80: the order in scores stayed the same.

If you keep list.sort(...) inside, tsc stops it with error TS2339: readonly number[] has no sort method. That way the promise in the signature is checked on every line of the function.

Only tsc checks it

readonly is part of the type, and types are erased. Nothing of readonly number[] is left in the JavaScript file, and at run time the array is an ordinary one: push works. Only tsc's check before the run protects it.

Here the display-case analogy stops working. A display case is closed to everyone, while readonly closes the array only to whoever looks at it through this type. After const view: readonly number[] = scores;, the call scores.push(60) goes through, since scores has the type number[], and view.length becomes 4.

Interviewers often ask how readonly number[] differs from const:

  • const scores forbids putting another array into the name scores, but allows scores.push(60);
  • readonly number[] forbids changing the array itself, but a let variable of this type can be given another array.

Together, in const scores: readonly number[], they close both the name and the contents.

Common mistake

A read-only array was passed to a function with an ordinary number[] parameter:

TypeScript
function total(list: number[]) {
    let sum = 0;
    for (const n of list) {
        sum += n;
    }
    return sum;
}

const scores: readonly number[] = [70, 95, 80];
console.log(total(scores)); // ✗
TEXT
error TS4104: The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.

tsc answers that the type readonly number[] is read-only and cannot be assigned to the mutable type number[]. A number[] parameter lets the function change the array, and scores must not be changed. The function total does not change the array, so the fix is to give it the same promise: function total(list: readonly number[]).

The rule works one way: an ordinary array fits a readonly number[] parameter, while a read-only array does not fit a number[] parameter.

Key points

  • readonly number[] is an array you can read but not change: push gives error TS2339, a write by index TS2542.
  • readonly on a parameter promises not to touch the calling code's array; an ordinary array can be passed to such a parameter.
  • A read-only array does not fit a number[] parameter: error TS4104.
  • const protects the variable's name, readonly the array's contents, and only tsc checks it.