Массивы и кортежи
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
Тип массива в TypeScript получается из типа элемента добавлением двух символов: number[]. Но рядом живут ещё два типа — readonly number[], у которого просто нет push, и кортеж [number, number], где длина записана прямо в типе. Разница между ними становится видна ровно в тот момент, когда компилятор отказывается что-то принимать.
Тип массива — это тип элемента плюс квадратные скобки. Запись number[] читается «массив чисел» и содержит ровно одно обещание: сколько бы элементов ни лежало внутри, каждый из них — число. Про длину не сказано ничего, и это не оговорка, а суть: массивы растут и укорачиваются.
Компилятор сказал Аргумент типа "string" нельзя назначить параметру типа "number" — и всё равно строка уехала в массив. Так работает tsc по умолчанию: типы стираются при сборке, до браузера доезжает обычный JavaScript, и предупреждение выполнение не отменяет. К этой мысли мы будем возвращаться всю часть.
Вторая запись того же типа — Array<string>. Она эквивалентна string[] до последней буквы; короткая форма привычнее, длинная иногда читается лучше, когда тип элемента сам длинный.
Если аннотации нет, тип выводится из литерала — тот самый вывод, который мы разбирали в первой главе. Из разнородного литерала получается массив объединения: не «массив чего попало», а «массив вот этих трёх типов».
Пустой литерал компилятор доращивает по ходу: const list = [] после первого push(1) считается массивом чисел. Внутри объекта этот механизм не работает — у { items: [] } поле остаётся never[], и положить туда не выйдет ничего.
Иногда массив используют не как список, а как пару: координаты точки, ширина и высота, ключ и значение. Порядок там значим, длина фиксирована, и элементы бывают разных типов. Для этого есть кортеж — [number, number]: квадратные скобки не после типа, а вокруг перечисления.
Диагностика на лишний элемент устроена в два этажа: сначала Тип "[number, number, number]" не может быть назначен для типа "[number, number]", а под ней причина — Число элементов в источнике — 3, но целевой объект разрешает только 2. Вложенная причина в сообщениях компилятора почти всегда полезнее верхней строки. На нехватку элемента будет та же ошибка с другим окончанием: «целевой объект требует 2».
Порядок в кортеже тоже часть типа. И обращение за границу компилятор ловит отдельным кодом — TS2493, которого у обычного массива не бывает в принципе.
Элементам кортежа можно дать имена. На поведение кода они не влияют совсем — это подсказка для человека и для редактора, чтобы в подсказке было видно x и y, а не «первый» и «второй».
Кортеж не обязан быть жёстко фиксированной длины. Знак вопроса делает хвостовой элемент необязательным, а ... позволяет дописать сколько угодно однотипных значений — как rest-параметр из части I, только в типе.
Значки ставятся вокруг имени по-разному: вопросительный знак — сразу после имени (alpha?: number), а многоточие — перед ним (...args: number[]). Перепутаете — компилятор отдельным сообщением объяснит, куда их вернуть.
Первое, что удивляет: обычный литерал кортежем не становится. Компилятор видит [10, 20] и выводит number[] — потому что массив обычно растёт, и предполагать фиксированную длину было бы самонадеянно. Кортеж — всегда осознанное решение автора.
Причина в вложенной строке: Целевой объект требует следующего числа элементов — 2, но источник может иметь меньше. Компилятор не спорит, что сейчас в pair ровно два числа. Он говорит про тип: number[] не обещает длину, а кортеж требует гарантии.
Второе удивляет сильнее. Под кортежем лежит настоящий массив, и все методы массива у него на месте — включая те, что меняют длину.
push прошёл без единого слова, длина стала тройкой, тридцатка честно напечаталась — а чтение point[2] компилятор запретил. Это известная дыра в системе типов: методы вроде push и pop у кортежа доступны, и на них проверка длины не распространяется. Ловится только то, что записано в типе, а тип после push не изменился.
Кортеж защищает от опечатки при создании, а не от мутации в рантайме. Защиту от мутации даёт readonly — следующий раздел.
Слово readonly перед типом массива убирает из него всё, что меняет содержимое: push, pop, sort, splice, присваивание по индексу. Формально это отдельный тип ReadonlyArray<T>, у которого таких методов в объявлении нет.
Сообщение звучит непривычно: не «нельзя менять», а Свойство "push" не существует в типе "readonly number[]". Компилятор не запрещает мутацию — он просто не показывает инструментов для неё. А после запуска длина стала тройкой: readonly живёт только до сборки, в браузере это самый обычный массив.
Главная польза — в параметрах функций. readonly на параметре превращает «я тут ничего не порчу» из комментария в проверяемое обещание.
Совместимость односторонняя: 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: неизменяемость в типах» в главе про объекты.
Обратная сторона: readonly-кортеж не примут там, где параметр объявлен изменяемым. Результат as const в функцию с параметром [number, number] просто так не передать — придётся либо смягчить параметр до readonly, либо копировать.
Самое опасное свойство типа T[] вытекает прямо из его определения: длины в типе нет, поэтому обращение по любому индексу компилятор считает элементом. По любому — включая тот, которого в массиве не существует.
Ни одной диагностики — и TypeError при запуске. Заметьте: никто не обманывал компилятор, он сам вывел string по правилам, которые сам же и объявил. В его картине мира массив бесконечен: раз индекс написан, значит, элемент есть.
Сравните с методами, у которых неудача записана в сигнатуре. at() и find() возвращают T | undefined, и на них строгий режим отрабатывает как надо.
TS18048 — Возможно, "found" имеет значение undefined. Ошибка ровно та же, что строкой выше, только на этот раз она поймана до запуска. Разница не в опасности кода, а в том, записана ли возможность промаха в типе.
Строгость к индексам включается отдельно — флагом 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[], и поэтому результат не удаётся положить в кортеж. Исправьте сигнатуру так, чтобы обещание «ровно два числа» было записано в типе.
Менять нужно ровно одно место — тип возврата. Тело функции уже возвращает два числа, компилятор проверит, что так оно и есть.
Заодно заработает деструктуризация с именами: const [low, high] = minMax(data) — теперь компилятор знает, что элементов ровно два и оба числа. С number[] он позволил бы достать и третий, которого нет.
Функция, которая портит чужой массив
highest возвращает наибольший результат — и попутно переставляет элементы в массиве вызывающего. Запустите и убедитесь в этом сами. Затем сделайте параметр readonly (компилятор сразу покажет виновника) и почините функцию так, чтобы исходный массив остался нетронутым.
После readonly компилятор скажет Свойство "sort" не существует в типе "readonly number[]": sort сортирует на месте, а этот тип таких методов не показывает. Лечится методом toSorted — он возвращает новый массив, оставляя исходный как был.
Обратите внимание: сам вызов highest(data) менять не пришлось. Обычный number[] спокойно передаётся в readonly-параметр — совместимость односторонняя, и это направление разрешено.
Цвет с необязательной прозрачностью
Тип Rgb описывает три канала цвета, а функция css уже готова принять и четвёртый — прозрачность. Сейчас компилятор ругается дважды: на деструктуризацию четвёртого элемента и на вызов с четырьмя аргументами. Допишите тип так, чтобы прозрачность была необязательной, а обе ошибки исчезли.
Необязательный элемент помечается вопросительным знаком. Имена давать не обязательно, но в кортеже из четырёх чисел они превращают подсказку редактора в осмысленную.
Теперь a при деструктуризации имеет тип number | undefined. Проверку a === undefined компилятор, кстати, не потребовал бы: склеить undefined со строкой он разрешает, и получилось бы rgb(255 0 0 / undefined). Тип честно сообщил, что элемента может не быть, — распорядиться этим знанием пришлось самому.