Кортежи
Ряд шкафчиков с одной табличкой подходит, когда все вещи одного рода. Но бывают данные другого устройства. Возьмите удостоверение: в первой графе написано имя, во второй стоит возраст. Граф ровно две, их порядок не меняется, и у каждой свой тип: имя — строка, возраст — число. Охранник читает первую графу как имя и не проверяет, имя ли там.
В JavaScript такие пары часто хранят в массиве: ["Аня", 30]. Но TypeScript видит в этом литерале обычный массив:
const pair = ["Аня", 30];
const first = pair[0];
console.log(first.toUpperCase()); // ✗error TS2339: Property 'toUpperCase' does not exist on type 'string | number'.Вы знаете, что на нулевом месте лежит имя. TypeScript этого не знает: для литерала ["Аня", 30] он вывел (string | number)[], массив, где любой элемент — строка или число. Поэтому pair[0] для него — string | number, а у числа метода toUpperCase нет.
Почему так? Массив может измениться: в него добавят элемент, его отсортируют. TypeScript не угадывает, что вы задумали пару, и выводит тип, который останется верным и после таких изменений.
Тип с местами для значений
Кортеж — массив заданной длины, где у каждого места свой тип. Тип кортежа записывают как список типов в квадратных скобках: [string, number] читается «на месте 0 строка, на месте 1 число».
const user: [string, number] = ["Аня", 30];
console.log(user[0].toUpperCase()); // АНЯ
console.log(user[1].toFixed(1)); // 30.0
console.log(user.length); // 2Давайте разберём пример по строкам:
- Аннотация
[string, number]говорит, чтоuser— кортеж из двух мест. user[0]— этоstring, поэтомуtoUpperCaseработает и печатаетАНЯ.user[1]— этоnumber, иtoFixed(1)печатает30.0.- Длина кортежа известна заранее: у
user.lengthтип2, и программа печатает2.
Сам литерал не изменился — тот же ["Аня", 30]. Изменилась аннотация: кортеж получается, только когда вы сами сообщили о нём TypeScript.
За концом кортежа
В удостоверении нет третьей графы, а в кортеже [string, number] нет места 2. Чтение user[2] tsc не пропустит:
error TS2493: Tuple type '[string, number]' of length '2' has no element at index '2'.Сообщение значит: «у кортежа [string, number] длины 2 нет элемента с индексом 2». В JavaScript такое чтение молча вернуло бы undefined.
Главное в этом уроке — откуда берётся кортеж. Литерал TypeScript сам по себе выводит как массив, а кортежем значение становится по аннотации. Пройдите программу по шагам и сравните тип pair с типом user, а затем типы их элементов:
1const pair = ["Аня", 30];подсказка редактораpair: (string | number)[]2const first = pair[0];3const user: [string, number] = ["Аня", 30];4const who = user[0];5const age = user[1];6const extra = user[2];
Из литерала TypeScript вывел массив, где любой элемент — строка или число.
Значение в pair и в user одно и то же, а типы разные: без аннотации — массив, с аннотацией — кортеж.
Деструктуризация кортежа
Кортеж удобно разбирать деструктуризацией: каждая переменная получает тип своего места. Так функция может вернуть сразу два значения:
function minMax(nums: number[]): [number, number] {
return [Math.min(...nums), Math.max(...nums)];
}
const [low, high] = minMax([3, 9, 1]);
console.log(low, high); // 1 9Разберём, что здесь происходит:
- Тип результата
[number, number]обещает пару: минимум и максимум. const [low, high]раскладывает пару по двум переменным, и у обеих типnumber.- Программа печатает
1 9.
Аннотация результата здесь нужна. Без неё TypeScript вывел бы из return массив number[], и вызывающий код не знал бы, что элементов ровно два.
Этот вопрос любят на собеседованиях: чем кортеж отличается от массива. У массива длина любая, а тип элемента один на все места. У кортежа заданы и длина, и тип каждого места.
Необязательное место
Последние места кортежа можно сделать необязательными знаком ?: в [string, number?] возраст можно не указывать. После const guest: [string, number?] = ["Борис"]; у guest.length тип 1 | 2, а guest[1] имеет тип number | undefined, и перед использованием его нужно проверить. Необязательные места идут только после обязательных.
Кортеж — всё ещё массив
Здесь аналогия с удостоверением заканчивается. Графу в удостоверение не добавишь, а кортеж остаётся массивом JavaScript. Вызов user.push(5) компилируется, и длина станет 3, хотя тип по-прежнему говорит о двух местах. Запретить такое можно так же, как у массива: readonly [string, number] — кортеж только для чтения, и push у него нет.
Частая ошибка
Пару сложили в переменную без аннотации, а потом передали туда, где ждут кортеж:
function greet(user: [string, number]) {
console.log(`${user[0]}, ${user[1]}`);
}
const ann = ["Аня", 30];
greet(ann); // ✗error TS2345: Argument of type '(string | number)[]' is not assignable to parameter of type '[string, number]'.У ann тип (string | number)[]: в таком массиве может лежать сколько угодно элементов в любом порядке, а greet ждёт ровно имя и возраст. Исправить можно двумя способами:
- написать аннотацию у переменной:
const ann: [string, number] = ["Аня", 30];; - передать литерал прямо в вызов: в
greet(["Борис", 25])TypeScript читает литерал по типу параметра и сразу видит кортеж.
Главное
- Кортеж — массив заданной длины со своим типом на каждом месте:
[string, number]. - Литерал
["Аня", 30]без аннотации TypeScript выводит как массив(string | number)[]. - Чтение за концом кортежа — ошибка
TS2493, аpushв кортеж компилируется, если он неreadonly. - Массив
(string | number)[]не подходит туда, где ждут[string, number]: нужна аннотация.