Перейти к содержимому
Теория · Массивы и кортежи · урок 3 из 7

Массивы только для чтения

В камере хранения бывает и витрина: ряд шкафчиков со стеклянными дверцами под замком. Посетитель видит, что лежит в каждом, может пересчитать вещи и прочитать номера. Но открыть дверцу, положить вещь или забрать её он не может.

Обычный массив похож на открытые шкафчики. Функция, которая получила массив, может его изменить, и вызывающий код об этом не узнает. Посмотрим на функцию, которая ищет лучший результат:

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

Разберём пример по строкам:

  1. best сортирует массив по убыванию и возвращает первый элемент.
  2. Метод sort не создаёт копию: он переставляет элементы в том же массиве.
  3. best(scores) печатает 95, как и ожидалось.
  4. А scores.join(" ") печатает 95 80 70: функция переставила массив, который ей дали только посмотреть.
  5. У a и b в (a, b) => b - a типа нет, но ошибки TS7006 тоже нет. TypeScript знает, что sort у number[] передаёт в эту функцию два числа, и сам даёт a и b тип number.

Такую ошибку трудно найти: порядок сломался в одном месте, а заметят это совсем в другом.

Тип readonly number[]

Слово readonly перед типом массива делает его массивом только для чтения: элементы можно читать, а менять сам массив нельзя.

В прошлой теме readonly стоял перед свойством. Перед типом его можно ставить только у массивов и кортежей: readonly User даст ошибку 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[]'.

Сообщение значит: «у типа readonly number[] нет свойства push». Методов, которые меняют массив, в этом типе нет совсем, поэтому tsc и пишет, что push не существует.

Что остаётся у массива только для чтения, а что пропадает:

  • нельзя вызвать push, pop, sort, reverse, splice — всё, что меняет сам массив;
  • нельзя записать по индексу: scores[0] = 60 даёт ошибку TS2542;
  • можно читать по индексу, узнавать length и обходить массив циклом for…of;
  • можно вызывать методы, которые возвращают новый массив: map, filter, slice, toSorted.

Как и у обычного массива, у этого типа есть длинная запись: ReadonlyArray<number> — тот же тип.

Параметр, который обещает не менять

Чаще всего readonly пишут у параметра функции. Такая аннотация — обещание в заголовке функции: «ваш массив я только прочитаю». Перепишем 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

Давайте посмотрим, что изменилось:

  1. list: readonly number[] — внутри функции массив можно только читать.
  2. toSorted возвращает отсортированную копию и не трогает исходный массив.
  3. В best передали обычный массив scores, и это разрешено: функция обещает делать с ним меньше, чем позволяет тип number[].
  4. Вторая строка печатает 70 95 80: порядок в scores остался прежним.

Если оставить внутри list.sort(...), tsc остановит его ошибкой TS2339: у readonly number[] нет метода sort. Так обещание из заголовка проверяется на каждой строке функции.

Проверяет только tsc

readonly — часть типа, а типы стираются. В JavaScript-файле от readonly number[] ничего не останется, и при запуске массив будет обычным: push сработает. Защищает только проверка tsc до запуска.

Здесь аналогия с витриной перестаёт работать. Витрина закрыта для всех, а readonly закрывает массив только для того, кто смотрит на него через этот тип. После const view: readonly number[] = scores; вызов scores.push(60) пройдёт, ведь у scores тип number[], и view.length станет 4.

На собеседованиях часто спрашивают, чем readonly number[] отличается от const:

  • const scores запрещает записать в имя scores другой массив, но разрешает scores.push(60);
  • readonly number[] запрещает менять сам массив, но переменной let такого типа можно присвоить другой массив.

Вместе, в const scores: readonly number[], они закрывают и имя, и содержимое.

Частая ошибка

Массив только для чтения передали в функцию с обычным параметром number[]:

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 отвечает: тип readonly number[] только для чтения, и его нельзя присвоить изменяемому типу number[]. Параметр number[] разрешает функции менять массив, а scores менять запрещено. Функция total массив не меняет, поэтому исправление — дать то же обещание и ей: function total(list: readonly number[]).

Правило работает в одну сторону: обычный массив подходит к параметру readonly number[], а массив только для чтения к параметру number[] — нет.

Главное

  • readonly number[] — массив, который можно читать, но нельзя менять: push даёт ошибку TS2339, запись по индексу — TS2542.
  • readonly у параметра обещает не трогать массив вызывающего кода; обычный массив в такой параметр передать можно.
  • Массив только для чтения не подходит к параметру number[]: ошибка TS4104.
  • const защищает имя переменной, readonly — содержимое массива, и проверяет его только tsc.