Определение массива с несколькими типами в TypeScript: union types vs кортежи
Разбираемся с типами массивов в TypeScript
Вы столкнулись с классической путаницей между union types (объединение типов) и tuple types (кортежи). Давайте разложим всё по полочкам.
1. Массив с элементами разных типов в произвольном порядке
Когда вам нужно, чтобы массив мог содержать и строки, и числа в любом порядке — используйте union type с синтаксисом массива:
// Массив, который может содержать строки ИЛИ числа
let tags: (string | number)[] = ["typescript", 42, "javascript", 100];
// Альтернативная запись (эквивалентна)
let tags2: Array<string | number> = ["typescript", 42];
// Можно добавлять любые элементы этих типов
tags.push("react"); // ✅ OK
tags.push(2024); // ✅ OK
// tags.push(true); // ❌ Ошибка: boolean не входит в объединение
Здесь (string | number)[] означает: «массив, каждый элемент которого — это строка ИЛИ число». Порядок не важен.
2. Кортеж (tuple) — фиксированная последовательность типов
Если важен порядок и количество элементов — используйте кортежи. Они задаются как [тип1, тип2, ...]:
// Кортеж: первый элемент - число, второй - строка
let user: [number, string] = [1, "Alice"];
// ⚠️ Ошибки при нарушении порядка
// let wrongUser: [number, string] = ["Alice", 1]; // ❌
// ⚠️ Ошибки при неверном количестве элементов
// let wrongUser2: [number, string] = [1]; // ❌
// let wrongUser3: [number, string] = [1, "A", "B"]; // ❌
Кортежи строго фиксируют:
- Количество элементов
- Тип каждого элемента по позиции
3. Массив кортежей
Если вам нужен список таких пар «ID + имя» — объявляйте массив кортежей:
// Массив кортежей [number, string]
let users: [number, string][] = [
[1, "Alice"],
[2, "Bob"],
[3, "Charlie"]
];
// Добавление нового кортежа
users.push([4, "Dave"]); // ✅ OK
// users.push(["Eve", 5]); // ❌ Ошибка: неверный порядок
// Доступ к элементам
const firstUser = users[0]; // тип: [number, string]
const userId = users[0][0]; // тип: number
const userName = users[0][1]; // тип: string
Синтаксис [number, string][] читается как: «массив, каждый элемент которого — кортеж [number, string]».
4. Дополнительные возможности кортежей
В TypeScript кортежи могут быть более гибкими:
// Необязательные элементы (через ?)
let optionalTuple: [number, string?] = [1]; // ✅
optionalTuple = [1, "optional"]; // ✅
// Rest-элементы (оставшиеся элементы одного типа)
let restTuple: [number, ...string[]] = [1, "a", "b", "c"];
// Labeled tuples (именованные элементы) — для читаемости
let labeled: [id: number, name: string] = [1, "Alice"];
5. Когда что использовать?
- Union type массив
(A | B)[]— когда порядок не важен, а важен только набор допустимых типов (например, список тегов, смешанные данные). - Кортеж
[A, B]— когда важна структура: фиксированное количество элементов с известными типами на каждой позиции (например, пара ключ-значение, координаты). - Массив кортежей
[A, B][]— когда нужно хранить много таких структурированных записей (например, список пользователей).
Надеюсь, теперь стало понятнее! TypeScript даёт мощные инструменты для точного описания данных — главное выбрать правильную конструкцию под задачу.
<<Альтернативный подход: использование readonly и as const
Если вы работаете с константными массивами, где типы известны на этапе компиляции, можно использовать as const для автоматического выведения кортежных типов:
// Без as const — тип (string | number)[]
const tags = ["typescript", 42, "javascript", 100];
// С as const — тип readonly ["typescript", 42, "javascript", 100]
const tagsConst = ["typescript", 42, "javascript", 100] as const;
// Теперь у каждого элемента точный тип
type FirstTag = typeof tagsConst[0]; // "typescript" (literal type)
type SecondTag = typeof tagsConst[1]; // 42 (literal type)
Ещё один полезный приём — использование generic helper type для проверки структуры массива:
type IsTuple<T> = T extends readonly [any, ...any] ? true : false;
type Test1 = IsTuple<[number, string]>; // true
type Test2 = IsTuple<number[]>; // false
Это может пригодиться, если вы пишете библиотеки или сложные типы. Но для повседневной работы достаточно базовых конструкций из первого ответа.