Кортеж в TypeScript это массив с фиксированным числом элементов, в котором у каждой позиции свой тип. [string, number] означает ровно два элемента: сначала строка, затем число. Типы пишутся в квадратных скобках в том порядке, в котором идут значения.
Во время выполнения кортеж это обычный массив JavaScript. Всё, что добавляет кортеж (фиксированная длина и тип на каждой позиции), проверяется компилятором и стирается.
Синтаксис кортежей
| Тип кортежа | Принимает | Тип length |
|---|---|---|
[string, number] | ровно строку, затем число | 2 |
[x: number, y: number] | то же, с метками для читаемости | 2 |
[number, number, number?] | 2 или 3 числа | 2 | 3 |
[string, ...number[]] | строку, затем любое количество чисел | number |
[...string[], number] | любое количество строк, затем число | number |
readonly [number, number] | пару, которую нельзя изменить | 2 |
[] | только пустой массив | 0 |
Каждая форма разобрана ниже. Стоит обратить внимание на тип length: у фиксированного кортежа это литеральный тип, поэтому компилятор знает, что pair.length равно ровно 2.
Что проверяет компилятор
Тип кортежа фиксирует количество элементов, их порядок и тип на каждой позиции. Ошибка в любом из них даёт ошибку компиляции:
index.ts(2,7): error TS2322: Type '[string]' is not assignable to type '[string, number]'.
Source has 1 element(s) but target requires 2.
index.ts(3,36): error TS2322: Type 'number' is not assignable to type 'string'.
index.ts(3,40): error TS2322: Type 'string' is not assignable to type 'number'.
index.ts(5,16): error TS2493: Tuple type '[string, number]' of length '2' has no element at index '2'.
Обычный массив последнюю ошибку никогда бы не поймал: для string[] выражение arr[2] просто имеет тип string, а во время выполнения оказывается undefined.
Именованные элементы кортежа
Метки описывают, что означает каждая позиция. Они никак не меняют тип и способ индексации, но редакторы показывают их во всплывающих подсказках и подсказках сигнатур, и [number, number] становится гораздо понятнее.
Начиная с TypeScript 5.2 можно пометить одни позиции и оставить другие без меток, как в [first: string, number]. Метки нужны только читателям: [x: number, y: number] и [number, number] это один и тот же тип, и они присваиваются друг другу.
Опциональные элементы
? после типа элемента делает позицию опциональной. Опциональные элементы должны идти после обязательных, и каждый из них расширяет тип length.
Чтение опционального элемента даёт T | undefined, поэтому перед арифметикой нужно значение по умолчанию в шаблоне деструктуризации (a = 1) или проверка.
Rest-элементы
Rest-элемент, ...T[], обозначает любое количество элементов типа T. Он может стоять в конце, в начале или в середине, но в кортеже может быть только один.
length кортежа с rest-элементом имеет тип number, потому что размер больше не фиксирован. Фиксированным остаётся расположение типизированных позиций.
Readonly-кортежи и as const
readonly [T, U] убирает push, pop, splice и присвоение по индексу, то есть делает то, чем и должно быть значение фиксированной длины. as const после литерала массива выводит readonly-кортеж литеральных типов.
(typeof SIZES)[number] превращает кортеж в объединение типов его элементов; этот приём описан на странице индексированные типы доступа. Readonly-кортеж нельзя передать в параметр с типом изменяемого кортежа, поэтому функции, которые только читают, должны принимать readonly [number, number].
Проверка readonly работает только во время компиляции. Во время выполнения массив не заморожен (присвоение выше действительно выполнилось, это видно в выводе), поэтому, если нужна гарантия во время выполнения, используйте Object.freeze.
Возврат кортежа из функции
Именно так, кортежем из нескольких значений, работает useState в React (const [value, setValue] = useState(0)). Подвох в том, что литерал массива в return выводится как массив, а не как кортеж.
index.ts(9,13): error TS2365: Operator '+' cannot be applied to types 'number | (() => number)' and 'number'.
index.ts(10,1): error TS2349: This expression is not callable.
Not all constituents of type 'number | (() => number)' are callable.
Type 'number' has no call signatures.
Функция возвращает (number | (() => number))[], поэтому оба имени после деструктуризации получают тип-объединение. Исправлений два: аннотировать возвращаемый тип или добавить as const.
Кортеж в качестве возвращаемого значения позволяет вызывающему коду назвать части как угодно. Если значений больше двух-трёх или порядок неочевиден, возвращайте объект: { count, increment } документирует себя сам.
Кортежи как параметры функций
Rest-параметр с типом кортежа описывает весь список аргументов, включая опциональные. Именно так встроенный служебный тип Parameters<T> представляет параметры функции.
Разворачивание кортежа в вызов проверяет каждый аргумент по позиции, чего разворачивание (string | number)[] сделать не может.
Кортеж и массив
Массив (string | number)[] | Кортеж [string, number] | |
|---|---|---|
| Длина | любая | фиксированная (или ограниченная опциональными и rest-элементами) |
Тип x[0] | string | number | string |
Тип x[5] | string | number | ошибка компиляции TS2493 |
Тип length | number | 2 |
| Порядок типов | не отслеживается | отслеживается |
| Значение во время выполнения | массив JavaScript | тот же массив JavaScript |
| Типичное применение | списки однородных элементов | небольшие фиксированные группы: пары, координаты, [key, value], несколько возвращаемых значений |
Кортежи встречаются и во встроенных типах. Object.entries(obj) возвращает [string, T][], а Map создаётся из кортежей [key, value]:
Одна ловушка: у изменяемого кортежа есть все методы массива, поэтому pair.push(3) компилируется на [string, number] и незаметно создаёт массив из трёх элементов, хотя тип говорит о двух. Объявление кортежей как readonly закрывает эту дыру. И раз типы стираются, данные извне программы (JSON, API) во время выполнения по типу кортежа не проверяются: прежде чем им доверять, проверьте длину и типы элементов.
Вариативные типы кортежей
Типы кортежей могут разворачивать другие типы кортежей, [...T, ...U]. Вместе с дженериками это позволяет типизировать функции, которые склеивают кортежи или добавляют элементы в начало, сохраняя каждую позицию:
Типы библиотек тоже опираются на вывод кортежей: Promise.all([fetchUser(), fetchPosts()]) разрешается в кортеж с отдельным типом для каждого входного промиса.
Часто задаваемые вопросы
Что такое кортеж в TypeScript?
Кортеж это тип массива фиксированной длины, в котором у каждой позиции свой тип: [string, number] это ровно два элемента, сначала строка, затем число. Во время выполнения это обычный массив JavaScript; длина и типы по позициям проверяются только во время компиляции.
Чем кортеж отличается от массива в TypeScript?
У типа массива вроде (string | number)[] любая длина, и у каждого элемента один и тот же тип (объединение), поэтому arr[0] имеет тип string | number. У кортежа вроде [string, number] длина известна, t[0] это string, t[1] это number, а t[2] даёт ошибку компиляции.
Как вернуть кортеж из функции в TypeScript?
Аннотируйте возвращаемый тип, function f(): [number, string], или завершите возвращаемое выражение as const, что даст readonly-кортеж. Без этого return [count, setCount] выводится как массив объединения, например (number | (() => void))[], и деструктуризация даёт типы-объединения.
Что такое именованные элементы кортежа?
Это метки на позициях, [name: string, age: number]. Они не меняют ни тип, ни способ доступа (по-прежнему t[0]), но редакторы показывают их во всплывающих подсказках и в подсказках параметров функций, параметры которых типизированы кортежем. Опциональные и rest-элементы работают с метками: [x: number, y?: number], [head: string, ...rest: number[]].
Можно ли делать push в кортеж в TypeScript?
В изменяемый кортеж можно: push компилируется, потому что кортежи наследуют методы массивов, хотя это нарушает фиксированную длину. Объявите кортеж как readonly (или создайте его через as const), и push, pop и присвоение по индексу станут ошибками компиляции.