Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Типи кортежів у TypeScript». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Тип кортеж (tuple)** у TypeScript - це масив фіксованої довжини де кожна позиція має свій конкретний тип. На відміну від звичайного масиву, компілятор відслідковує тип кожного індексу окремо і перевіряє довжину під час компіляції. ```typescript const user: [string, number] = ["Alice", 25]; user[0]; // string user[1]; // number user[2]; // Error: no element at index '2' ``` **Ключове:** в рантаймі кортеж - звичайний JavaScript-масив. Вся перевірка типів існує тільки в компіляторі.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Кортеж (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` по всій кодовій базі автоматично. Саме тут кортежі окупаються у великих проектах.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.