Skip to main content

Типи кортежів у TypeScript

Кортеж (tuple) у TypeScript - це масив фіксованої довжини, де кожна позиція має свій конкретний тип.

Теорія

TL;DR

  • Кортеж - це типізований масив де позиція важлива: [string, number] означає, що індекс 0 завжди string, індекс 1 завжди number
  • На відміну від звичайного масиву, компілятор перевіряє і кількість елементів, і тип кожного слота
  • useState в React повертає кортеж: [значення, setter]
  • Іменовані елементи ([x: number, y: number]) покращують читабельність без змін у рантаймі
  • Використовуй кортежі для значень що повертаються з функцій і структурованих пар, не для списків довільної довжини

Швидкий приклад

typescript
// Масив: будь-яка кількість рядків, всі одного типу const tags: string[] = ["ts", "react", "node"]; // Кортеж: рівно 2 елементи, кожен зі своїм типом const user: [string, number] = ["Alice", 25]; user[0]; // string user[1]; // number user[2]; // Error: Tuple type '[string, number]' has no element at index '2' // TypeScript ловить це під час компіляції, не в рантаймі const wrong: [string, number] = [25, "Alice"]; // Error

Головне: компілятор знає точний тип на кожному індексі. user[0] - це string, а не string | number.

Кортеж vs масив

Звичайний масив (string[]) каже: кожен елемент рядок, кількість довільна. Кортеж ([string, number]) каже: рівно два елементи, перший рядок, другий число. Ця різниця змінює те, як компілятор аналізує кожен слот.

З масивом arr[0] повертає string. З кортежем tup[0] теж повертає string, але tup[1] повертає number. Компілятор відслідковує кожну позицію окремо.

Іменовані елементи

typescript
type Point = [x: number, y: number]; type Range = [start: number, end: number]; type HttpResponse = [status: number, body: string]; const origin: Point = [0, 0];

Імена - для людини, не для компілятора. Point і [number, number] структурно ідентичні. Але деструктуризація const [x, y] = origin покаже x і y у підказках IDE. Варто додавати для кортежів з трьома і більше елементами.

Необов'язкові та rest-елементи

typescript
// Необов'язковий елемент (завжди в кінці) type Response = [number, string, string?]; const minimal: Response = [200, "OK"]; const full: Response = [200, "OK", "application/json"]; // Rest-елементи: перший рядок, далі довільна кількість чисел type StringThenNumbers = [string, ...number[]]; const scores: StringThenNumbers = ["player1", 90, 85, 92]; // Фіксовані кінці, гнучка середина type Padded = [string, ...number[], boolean]; const row: Padded = ["label", 1, 2, 3, true];

Необов'язкові елементи завжди в кінці. Один rest-елемент на кортеж, але він може бути посередині.

Кортежі тільки для читання

typescript
type ReadonlyPoint = readonly [number, number]; const p: ReadonlyPoint = [10, 20]; p[0] = 30; // Error: Cannot assign to '0' because it is a read-only property // as const створює readonly-кортеж з літеральними типами const coords = [10, 20] as const; // readonly [10, 20]

as const іде далі ніж просто readonly: звужує типи до літеральних значень (10 і 20, не просто number). Це важливо коли функція очікує readonly [10, 20] замість readonly [number, number].

Як компілятор обробляє кортежі

TypeScript компілює кортежі у звичайні JavaScript-масиви. Ніякого runtime-об'єкта "кортеж" не існує. Перевірка довжини, тип кожного індексу, обмеження readonly - все це живе тільки в системі типів.

Саме тому .push() на кортежі без readonly може не дати помилки компіляції. Компілятор перевіряє звернення за індексом, але методи мутації масиву проходять крізь. Використовуй readonly щоб заблокувати їх.

Типові помилки

Відсутня анотація - TypeScript виводить union-масив:

typescript
const pair = ["Alice", 25]; // TypeScript виводить: (string | number)[] — не кортеж! function greet(person: [string, number]) {} greet(pair); // Error: Argument of type '(string | number)[]' is not // assignable to parameter of type '[string, number]' // Виправлення: явна анотація const pair: [string, number] = ["Alice", 25];

Мутація кортежу без readonly:

typescript
const coords: [number, number] = [10, 20]; coords.push(30); // Компілюється без помилки, але порушує контракт довжини const safe: readonly [number, number] = [10, 20]; safe.push(30); // Error: Property 'push' does not exist on type 'readonly [number, number]'

Плутанина між as const і явною анотацією:

typescript
const a = [1, "hello"] as const; // readonly [1, "hello"] — літеральні типи const b: [number, string] = [1, "hello"]; // [number, string] — ширші типи // a[0] має тип '1', b[0] має тип 'number'

as const потрібен для літеральних типів, явна анотація - для ширших.

Необов'язковий елемент не в кінці:

typescript
type Wrong = [string, number?, boolean]; // Error: A required element cannot follow an optional element type Right = [string, boolean, number?]; // OK

Де зустрічається в реальному коді

  • React useState повертає [S, Dispatch<SetStateAction<S>>] - це кортеж
  • useReducer повертає [state, dispatch]
  • Go-стиль обробки помилок: function fetchUser(): Promise<[User | null, Error | null]>
  • D3 та бібліотеки для графіків використовують [number, number] для координат
  • Варіативні типи кортежів лежать в основі типізованих compose і pipe у функціональних бібліотеках

Питання від інтерв'юера

Q: Чому TypeScript виводить (string | number)[] замість [string, number] коли я пишу const x = ["Alice", 25]?
A: TypeScript розширює літерали масивів до найзагальнішого типу, бо масиви мутабельні і можуть змінювати довжину. Для кортежу потрібна явна анотація або as const.

Q: Що таке варіативні типи кортежів (variadic tuple types)?
A: Варіативні кортежі використовують spread у позиції типу: type Concat<A extends unknown[], B extends unknown[]> = [...A, ...B]. Це дозволяє будувати нові типи кортежів з існуючих. Бібліотеки використовують це для типізованих compose, pipe і middleware-ланцюгів де потрібно відслідковувати повний список аргументів.

Q: Чи можуть кортежі розширювати інші кортежі?
A: Прямого extends як у класів немає. Але можна зробити spread: type Extended = [...Base, string]. Саме на цьому будуються варіативні кортежі.

Q: Що трапиться якщо викликати .push() на readonly-кортежі?
A: TypeScript видасть помилку: Property 'push' does not exist on type 'readonly [number, number]'. На звичайному кортежі .push() компілюється, але порушує контракт довжини в рантаймі.

Q: Чим відрізняється деструктуризація кортежу від деструктуризації об'єкта?
A: З кортежами імена беруться з патерну деструктуризації, а не з типу. const [a, b] = point працює незалежно від того, чи є іменовані елементи. З об'єктами назви властивостей - частина контракту типу.

Приклади

Повернення координат з функції

typescript
function parseLatLng(input: string): [number, number] | null { const parts = input.split(",").map(Number); if (parts.length !== 2 || parts.some(isNaN)) return null; return [parts[0], parts[1]]; } const result = parseLatLng("48.8566,2.3522"); if (result) { const [lat, lng] = result; console.log(`Париж: ${lat}, ${lng}`); }

Кортеж тут кращий ніж об'єкт, коли пара має очевидні позиційні відносини і не буде рости новими полями. Call site деструктурує природно.

Go-стиль обробки помилок

typescript
async function fetchUser(id: string): Promise<[User | null, Error | null]> { try { const user = await db.users.findById(id); return [user, null]; } catch (err) { return [null, err instanceof Error ? err : new Error(String(err))]; } } const [user, error] = await fetchUser("123"); if (error) { console.error(error.message); return; } // TypeScript знає що user тут не null console.log(user.name);

Патерн робить обробку помилок явною на місці виклику без try-catch блоків. Конвенція: або перший, або другий елемент null, ніколи обидва. TypeScript це не перевіряє автоматично, тому команда має домовитись.

Типізована система подій з варіативними кортежами

typescript
type EventMap = { resize: [width: number, height: number]; keydown: [key: string, modifiers: string[]]; submit: [data: FormData]; }; function emit<K extends keyof EventMap>(event: K, ...args: EventMap[K]): void { // відправка аргументів до зареєстрованих слухачів } emit("resize", 1920, 1080); // OK emit("keydown", "Enter", ["ctrl"]); // OK emit("resize", "wide", 1080); // Error: 'string' not assignable to 'number'

Назва події і типи аргументів пов'язані в одному місці. Додай нову подію до мапи - компілятор перевірить кожен виклик emit по всій кодовій базі автоматично. Саме тут кортежі окупаються у великих проектах.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?