Avatar ·

Определение массива с несколькими типами в TypeScript: union types vs кортежи

Всем привет! Я недавно начал изучать TypeScript и столкнулся с проблемой при объявлении массива, который может содержать элементы разных типов. У меня есть функция, которая принимает массив строк и чисел. Например, я хочу хранить ID (число) и имя (строку) пользователя в одном массиве. Вот как я пробовал это сделать: ```typescript let users = [1, "Alice", 2, "Bob"]; ``` TypeScript автоматически выводит тип `(string | number)[]`, что вроде бы работает. Но потом мне понадобилось строго контролировать порядок: сначала число, потом строка. Я попробовал так: ```typescript let user: [number, string] = [1, "Alice"]; ``` Тут всё хорошо. Но что делать, если я хочу массив из таких кортежей? Я написал: ```typescript let users: [number, string][] = [[1, "Alice"], [2, "Bob"]]; ``` Но потом возникла другая задача: массив может содержать ИЛИ строки, ИЛИ числа, но не одновременно в одном элементе. Например, список тегов: `["typescript", 42, "javascript", 100]`. Вопрос: как правильно объявить массив, который: 1. Содержит элементы только одного из двух типов в произвольном порядке (например, `(string | number)[]`). 2. Содержит фиксированную последовательность типов (кортеж), как `[number, string]`. 3. Содержит массив кортежей. Я путаюсь в синтаксисе union types и tuple types. Объясните, пожалуйста, разницу и когда что использовать. Заранее спасибо! <<>>

Разбираемся с типами массивов в 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

Это может пригодиться, если вы пишете библиотеки или сложные типы. Но для повседневной работы достаточно базовых конструкций из первого ответа.