Массивы и кортежи

базовые типы≈ 9 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

Тип массива в TypeScript получается из типа элемента добавлением двух символов: number[]. Но рядом живут ещё два типа — readonly number[], у которого просто нет push, и кортеж [number, number], где длина записана прямо в типе. Разница между ними становится видна ровно в тот момент, когда компилятор отказывается что-то принимать.

Разделы темы:·

Тип массива — это тип элемента плюс квадратные скобки. Запись number[] читается «массив чисел» и содержит ровно одно обещание: сколько бы элементов ни лежало внутри, каждый из них — число. Про длину не сказано ничего, и это не оговорка, а суть: массивы растут и укорачиваются.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Компилятор сказал Аргумент типа "string" нельзя назначить параметру типа "number" — и всё равно строка уехала в массив. Так работает tsc по умолчанию: типы стираются при сборке, до браузера доезжает обычный JavaScript, и предупреждение выполнение не отменяет. К этой мысли мы будем возвращаться всю часть.

Вторая запись того же типа — Array<string>. Она эквивалентна string[] до последней буквы; короткая форма привычнее, длинная иногда читается лучше, когда тип элемента сам длинный.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Если аннотации нет, тип выводится из литерала — тот самый вывод, который мы разбирали в первой главе. Из разнородного литерала получается массив объединения: не «массив чего попало», а «массив вот этих трёх типов».

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Пустой литерал компилятор доращивает по ходу: const list = [] после первого push(1) считается массивом чисел. Внутри объекта этот механизм не работает — у { items: [] } поле остаётся never[], и положить туда не выйдет ничего.

Иногда массив используют не как список, а как пару: координаты точки, ширина и высота, ключ и значение. Порядок там значим, длина фиксирована, и элементы бывают разных типов. Для этого есть кортеж — [number, number]: квадратные скобки не после типа, а вокруг перечисления.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Диагностика на лишний элемент устроена в два этажа: сначала Тип "[number, number, number]" не может быть назначен для типа "[number, number]", а под ней причина — Число элементов в источнике — 3, но целевой объект разрешает только 2. Вложенная причина в сообщениях компилятора почти всегда полезнее верхней строки. На нехватку элемента будет та же ошибка с другим окончанием: «целевой объект требует 2».

Порядок в кортеже тоже часть типа. И обращение за границу компилятор ловит отдельным кодом — TS2493, которого у обычного массива не бывает в принципе.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Элементам кортежа можно дать имена. На поведение кода они не влияют совсем — это подсказка для человека и для редактора, чтобы в подсказке было видно x и y, а не «первый» и «второй».

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Кортеж не обязан быть жёстко фиксированной длины. Знак вопроса делает хвостовой элемент необязательным, а ... позволяет дописать сколько угодно однотипных значений — как rest-параметр из части I, только в типе.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Значки ставятся вокруг имени по-разному: вопросительный знак — сразу после имени (alpha?: number), а многоточие — перед ним (...args: number[]). Перепутаете — компилятор отдельным сообщением объяснит, куда их вернуть.

Первое, что удивляет: обычный литерал кортежем не становится. Компилятор видит [10, 20] и выводит number[] — потому что массив обычно растёт, и предполагать фиксированную длину было бы самонадеянно. Кортеж — всегда осознанное решение автора.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Причина в вложенной строке: Целевой объект требует следующего числа элементов — 2, но источник может иметь меньше. Компилятор не спорит, что сейчас в pair ровно два числа. Он говорит про тип: number[] не обещает длину, а кортеж требует гарантии.

Второе удивляет сильнее. Под кортежем лежит настоящий массив, и все методы массива у него на месте — включая те, что меняют длину.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

push прошёл без единого слова, длина стала тройкой, тридцатка честно напечаталась — а чтение point[2] компилятор запретил. Это известная дыра в системе типов: методы вроде push и pop у кортежа доступны, и на них проверка длины не распространяется. Ловится только то, что записано в типе, а тип после push не изменился.

Кортеж защищает от опечатки при создании, а не от мутации в рантайме. Защиту от мутации даёт readonly — следующий раздел.

Слово readonly перед типом массива убирает из него всё, что меняет содержимое: push, pop, sort, splice, присваивание по индексу. Формально это отдельный тип ReadonlyArray<T>, у которого таких методов в объявлении нет.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Сообщение звучит непривычно: не «нельзя менять», а Свойство "push" не существует в типе "readonly number[]". Компилятор не запрещает мутацию — он просто не показывает инструментов для неё. А после запуска длина стала тройкой: readonly живёт только до сборки, в браузере это самый обычный массив.

Главная польза — в параметрах функций. readonly на параметре превращает «я тут ничего не порчу» из комментария в проверяемое обещание.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Совместимость односторонняя: number[] подходит там, где ждут readonly number[], но не наоборот. Последняя строка даёт TS4104 — Тип "readonly number[]" является "readonly" и не может быть назначен изменяемому типу "number[]". Разрешить такое присваивание значило бы отменить всю затею: одна строка — и массив снова можно портить.

ТипДлинаЧто разрешеноКогда брать
number[]любаявсёобычный список, который меняют
readonly number[]любаятолько чтениепараметр, который функция не портит
[number, number]ровно 2менять элементыпара: координаты, размер, ключ и значение
readonly [number, number]ровно 2только чтениеконстанта-пара, результат as const

С кортежем readonly работает так же и запрещает даже присваивание по индексу. Тот же результат одним словом даёт as const у литерала — ему отведена статья «readonly и as const: неизменяемость в типах» в главе про объекты.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Обратная сторона: readonly-кортеж не примут там, где параметр объявлен изменяемым. Результат as const в функцию с параметром [number, number] просто так не передать — придётся либо смягчить параметр до readonly, либо копировать.

Самое опасное свойство типа T[] вытекает прямо из его определения: длины в типе нет, поэтому обращение по любому индексу компилятор считает элементом. По любому — включая тот, которого в массиве не существует.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Ни одной диагностики — и TypeError при запуске. Заметьте: никто не обманывал компилятор, он сам вывел string по правилам, которые сам же и объявил. В его картине мира массив бесконечен: раз индекс написан, значит, элемент есть.

Сравните с методами, у которых неудача записана в сигнатуре. at() и find() возвращают T | undefined, и на них строгий режим отрабатывает как надо.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

TS18048 — Возможно, "found" имеет значение undefined. Ошибка ровно та же, что строкой выше, только на этот раз она поймана до запуска. Разница не в опасности кода, а в том, записана ли возможность промаха в типе.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Строгость к индексам включается отдельно — флагом noUncheckedIndexedAccess, который не входит в strict. С ним names[5] получает тип string | undefined, и компилятор начинает требовать проверку. Флаг стоит денег: проверять придётся каждое обращение по индексу, даже внутри цикла. Разберём его в главе про настройку проекта, в статье про строгость сверх strict.

Пока флага нет, помните простое правило: индекс — это обещание, которое даёте вы, а не компилятор.

  • T[] и Array<T> — одна и та же запись: тип элементов и обещание, что все элементы такие. Про длину массив не говорит ничего.
  • Кортеж [number, number] фиксирует длину, порядок и типы по местам: лишний элемент — TS2322 с числом элементов в причине, чтение за границей — TS2493.
  • Литерал сам кортежем не становится: [10, 20] выводится как number[]. Кортеж задаётся аннотацией или as const.
  • Кортеж защищает от опечатки при создании, но не от мутации: push у него есть, и после него тип остаётся прежним.
  • readonly T[] убирает мутирующие методы и не присваивается в number[] (TS4104), но существует только до запуска — в браузере это обычный массив.
  • Обращение по индексу компилятор считает элементом и промаха не видит; at() и find() честно возвращают T | undefined.

1/4В константе point лежит кортеж [number, number]. Строку point.push(30) компилятор пропустил молча. Что он скажет на следующую строку — console.log(point[2])?

2/4У переменной limits тип readonly number[]. Строка limits.push(30) даёт ошибку компилятора. Что случится при запуске — ведь ошибка выполнение не отменяет?

3/4Какой тип компилятор выведет для const pair = [10, 20], если аннотации нет?

4/4В строгом режиме берём names[5] из массива string[] длиной 2 и вызываем у результата toUpperCase(). Что произойдёт?

Пара вместо массива

Функция minMax возвращает минимум и максимум. Тип возврата у неё number[], и поэтому результат не удаётся положить в кортеж. Исправьте сигнатуру так, чтобы обещание «ровно два числа» было записано в типе.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Менять нужно ровно одно место — тип возврата. Тело функции уже возвращает два числа, компилятор проверит, что так оно и есть.

tsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Заодно заработает деструктуризация с именами: const [low, high] = minMax(data) — теперь компилятор знает, что элементов ровно два и оба числа. С number[] он позволил бы достать и третий, которого нет.

Функция, которая портит чужой массив

highest возвращает наибольший результат — и попутно переставляет элементы в массиве вызывающего. Запустите и убедитесь в этом сами. Затем сделайте параметр readonly (компилятор сразу покажет виновника) и почините функцию так, чтобы исходный массив остался нетронутым.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

После readonly компилятор скажет Свойство "sort" не существует в типе "readonly number[]": sort сортирует на месте, а этот тип таких методов не показывает. Лечится методом toSorted — он возвращает новый массив, оставляя исходный как был.

tsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Обратите внимание: сам вызов highest(data) менять не пришлось. Обычный number[] спокойно передаётся в readonly-параметр — совместимость односторонняя, и это направление разрешено.

Цвет с необязательной прозрачностью

Тип Rgb описывает три канала цвета, а функция css уже готова принять и четвёртый — прозрачность. Сейчас компилятор ругается дважды: на деструктуризацию четвёртого элемента и на вызов с четырьмя аргументами. Допишите тип так, чтобы прозрачность была необязательной, а обе ошибки исчезли.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Необязательный элемент помечается вопросительным знаком. Имена давать не обязательно, но в кортеже из четырёх чисел они превращают подсказку редактора в осмысленную.

tsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Теперь a при деструктуризации имеет тип number | undefined. Проверку a === undefined компилятор, кстати, не потребовал бы: склеить undefined со строкой он разрешает, и получилось бы rgb(255 0 0 / undefined). Тип честно сообщил, что элемента может не быть, — распорядиться этим знанием пришлось самому.