Введение: почему один тип — это скучно?
Представьте: вы пишете на TypeScript и вдруг сталкиваетесь с ситуацией, когда массив должен хранить данные разных типов. Например, список, где каждый элемент — это либо string, либо number. Или кортеж из имени (строка) и возраста (число).
Многие новички (и не только) задаются вопросом: «Как объявить массив с несколькими типами в TypeScript?» Этот вопрос набрал тысячи просмотров на Stack Overflow — и неспроста. TypeScript строг к типам, но при этом даёт гибкие инструменты для таких случаев.
В этой статье мы разберём все способы работы с массивами смешанных типов: от простых union-типов до кортежей и дженериков. Вы узнаете, когда какой подход использовать, и как не попасть в ловушку any.
1. Union Types: самый простой способ
Самый очевидный вариант — использовать union type (объединение типов). Это когда вы говорите: «Массив может содержать элементы типа string ИЛИ number».
// Массив, который может содержать строки или числа
const mixedArray: (string | number)[] = ['hello', 42, 'world', 100];
// Альтернативная запись (эквивалентна)
const mixedArray2: Array<string | number> = ['hello', 42, 'world', 100];
Здесь (string | number)[] означает: «массив, каждый элемент которого — строка или число». TypeScript будет проверять, что вы не попытаетесь добавить, скажем, булево значение или объект.
Когда это удобно?
- Когда порядок элементов не важен, а важен только набор допустимых типов.
- Когда вы работаете с API, которое возвращает массив разнородных данных.
- Когда вы храните список ID (числа) и имён (строки) в одном массиве.
Подводный камень
При использовании union-типов вы не можете гарантировать, что на первой позиции будет строка, а на второй — число. Типизация говорит только: «в массиве есть строки и числа, но в каком порядке — неизвестно».
const arr: (string | number)[] = [1, 'hello'];
// TypeScript не знает: arr[0] — это number или string?
const first = arr[0]; // тип: string | number
// Чтобы использовать как число, нужно сужать тип:
if (typeof first === 'number') {
console.log(first.toFixed(2));
}
2. Кортежи (Tuples): когда важен порядок
Если вам нужно, чтобы массив имел фиксированную структуру — например, первым идёт строка, вторым число, третьим булево значение — используйте кортежи (tuple types).
// Кортеж: [строка, число, булево]
const person: [string, number, boolean] = ['Alice', 30, true];
// Кортеж с метками (labels) — более читаемо
const product: [name: string, price: number, inStock: boolean] = ['Laptop', 1200, true];
Кортежи строго фиксируют количество элементов и тип на каждой позиции. Если вы попытаетесь присвоить значение не того типа или добавить лишний элемент — TypeScript выдаст ошибку.
Rest-элементы в кортежах
С TypeScript 4.0 появилась возможность указывать rest-элементы в кортежах. Это удобно, когда часть структуры фиксирована, а остальное — вариативно:
// Первый элемент — строка, остальные — числа
const data: [string, ...number[]] = ['scores', 95, 87, 92];
// Можно комбинировать
const logEntry: [timestamp: Date, ...messages: string[]] = [new Date(), 'info', 'user logged in'];
3. Type Alias для сложных массивов
Когда структура массива становится сложной, удобно вынести тип в отдельный alias (псевдоним). Это улучшает читаемость кода и позволяет переиспользовать тип.
type UserEntry = [id: number, name: string, email: string, isActive: boolean];
const users: UserEntry[] = [
[1, 'Alice', 'alice@example.com', true],
[2, 'Bob', 'bob@example.com', false]
];
// Функция, работающая с таким массивом
function printUser(user: UserEntry): void {
const [id, name] = user;
console.log(`#${id}: ${name}`);
}
Обратите внимание: UserEntry[] — это массив кортежей. То есть каждый элемент массива — это кортеж из 4 элементов.
4. Дженерики: максимальная гибкость
Если вы пишете функцию или класс, который должен работать с массивами произвольных типов, используйте дженерики (generics). Это не совсем «массив с несколькими типами», но позволяет создавать универсальные структуры данных.
// Функция, принимающая массив элементов любого типа
function firstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
const num = firstElement([1, 2, 3]); // тип number
const str = firstElement(['a', 'b']); // тип string
// Функция для массива из двух разных типов
function pair<A, B>(a: A, b: B): [A, B] {
return [a, b];
}
const result = pair('hello', 42); // тип [string, number]
Дженерики особенно полезны, когда вы создаёте библиотеки или утилиты, которые должны работать с любыми типами данных, сохраняя при этом строгую типизацию.
5. Частые ошибки и как их избежать
Ошибка 1: Использование any
Самая распространённая ошибка новичков — написать const arr: any[] = [...]. Это отключает всю систему типов. Избегайте any как чумы!
// ПЛОХО — теряем все преимущества TypeScript
const bad: any[] = [1, 'hello', { a: 1 }];
// ХОРОШО — явно указываем допустимые типы
const good: (number | string | { a: number })[] = [1, 'hello', { a: 1 }];
Ошибка 2: Путаница между union и кортежем
Запомните правило: если порядок и количество элементов важны — кортеж. Если нет — union.
// Неправильно: хотели кортеж, но получили union
const coords: (number | string)[] = [10, 'y']; // работает, но не строго
// Правильно: кортеж с двумя элементами
const coords2: [number, string] = [10, 'y'];
Ошибка 3: Забыли про readonly
Если массив не должен изменяться, используйте readonly:
const config: readonly [string, number] = ['timeout', 5000];
// config[