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

Кортежи

Ряд шкафчиков с одной табличкой подходит, когда все вещи одного рода. Но бывают данные другого устройства. Возьмите удостоверение: в первой графе написано имя, во второй стоит возраст. Граф ровно две, их порядок не меняется, и у каждой свой тип: имя — строка, возраст — число. Охранник читает первую графу как имя и не проверяет, имя ли там.

В JavaScript такие пары часто хранят в массиве: ["Аня", 30]. Но TypeScript видит в этом литерале обычный массив:

TypeScript
const pair = ["Аня", 30];
const first = pair[0];
console.log(first.toUpperCase()); // ✗
TEXT
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 число».

TypeScript
const user: [string, number] = ["Аня", 30];
console.log(user[0].toUpperCase()); // АНЯ
console.log(user[1].toFixed(1)); // 30.0
console.log(user.length); // 2

Давайте разберём пример по строкам:

  1. Аннотация [string, number] говорит, что user — кортеж из двух мест.
  2. user[0] — это string, поэтому toUpperCase работает и печатает АНЯ.
  3. user[1] — это number, и toFixed(1) печатает 30.0.
  4. Длина кортежа известна заранее: у user.length тип 2, и программа печатает 2.

Сам литерал не изменился — тот же ["Аня", 30]. Изменилась аннотация: кортеж получается, только когда вы сами сообщили о нём TypeScript.

За концом кортежа

В удостоверении нет третьей графы, а в кортеже [string, number] нет места 2. Чтение user[2] tsc не пропустит:

TEXT
error TS2493: Tuple type '[string, number]' of length '2' has no element at index '2'.

Сообщение значит: «у кортежа [string, number] длины 2 нет элемента с индексом 2». В JavaScript такое чтение молча вернуло бы undefined.

Главное в этом уроке — откуда берётся кортеж. Литерал TypeScript сам по себе выводит как массив, а кортежем значение становится по аннотации. Пройдите программу по шагам и сравните тип pair с типом user, а затем типы их элементов:

ВживуюМассив или кортежшаг 1 из 6

Из литерала TypeScript вывел массив, где любой элемент — строка или число.

Нажимайте «Следующий шаг»: подсказка покажет тип или ошибку на выделенной строке

Значение в pair и в user одно и то же, а типы разные: без аннотации — массив, с аннотацией — кортеж.

Деструктуризация кортежа

Кортеж удобно разбирать деструктуризацией: каждая переменная получает тип своего места. Так функция может вернуть сразу два значения:

TypeScript
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

Разберём, что здесь происходит:

  1. Тип результата [number, number] обещает пару: минимум и максимум.
  2. const [low, high] раскладывает пару по двум переменным, и у обеих тип number.
  3. Программа печатает 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 у него нет.

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

Пару сложили в переменную без аннотации, а потом передали туда, где ждут кортеж:

TypeScript
function greet(user: [string, number]) {
    console.log(`${user[0]}, ${user[1]}`);
}

const ann = ["Аня", 30];
greet(ann); // ✗
TEXT
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]: нужна аннотация.