Массивы и кортежи
Типизированные массивы (Array<T>, T) и кортежи (tuple) — фиксированные по длине и типам массивы.
Зачем нужно
- Массивы — одна из самых частых структур данных
- Типизация массивов предотвращает ошибки при работе с элементами
- Кортежи позволяют описать массив с фиксированной структурой (координаты, пары ключ-значение)
- Readonly-массивы защищают данные от мутации
Где используется
- Работа с коллекциями данных в любом TypeScript-проекте
- API-ответы часто содержат массивы объектов
- React hooks возвращают кортежи:
[state, setState] - Функции с переменным числом аргументов используют variadic tuples
Предпосылки
- Примитивные типы — базовые типы данных
- Знание массивов в JavaScript
Типизация массивов
Два синтаксиса — равнозначны
// Синтаксис 1: T
let numbers: number = [1, 2, 3];
let names: string = ["Alice", "Bob"];
// Синтаксис 2: Array<T> (generic)
let numbers: Array<number> = [1, 2, 3];
let names: Array<string> = ["Alice", "Bob"];
// Оба варианта идентичны — выбирайте один стиль
Вывод типов для массивов
// TS выводит тип автоматически
const nums = [1, 2, 3]; // number
const mixed = [1, "hello", true]; // (string | number | boolean)
const empty: number = ; // Для пустого массива нужна аннотация
Массивы объектов
interface User {
id: number;
name: string;
}
const users: User = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
];
// Массив из union типов
const values: (string | number) = [1, "two", 3, "four"];
Многомерные массивы
const matrix: number = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
];
// Или с Array<>
const matrix: Array<Array<number>> = [[1, 2], [3, 4]];
Readonly массивы
Защита от мутации — нельзя менять элементы, добавлять или удалять:
// Способ 1: readonly T
const numbers: readonly number = [1, 2, 3];
// Способ 2: ReadonlyArray<T>
const names: ReadonlyArray<string> = ["Alice", "Bob"];
// Запрещено:
numbers.push(4); // Ошибка! Property 'push' does not exist
numbers[0] = 10; // Ошибка! Index signature in type 'readonly number' only permits reading
numbers.splice(0, 1); // Ошибка!
numbers.sort(); // Ошибка!
// Разрешено (не мутирующие методы):
const first = numbers[0]; // OK
const sliced = numbers.slice; // OK — возвращает новый массив
const mapped = numbers.map(n => n * 2); // OK
const filtered = numbers.filter(n => n > 1); // OK
as const для immutable массивов
// as const делает массив readonly с литеральными типами
const colors = ["red", "green", "blue"] as const;
// Тип: readonly ["red", "green", "blue"]
colors.push("yellow"); // Ошибка!
colors[0] = "pink"; // Ошибка!
// Полезно для создания union из значений массива
type Color = (typeof colors)[number]; // "red" | "green" | "blue"
Кортежи (Tuples)
Кортеж — массив с фиксированной длиной и типами для каждой позиции:
// Кортеж: [тип1, тип2, ...]
let point: [number, number] = [10, 20];
let entry: [string, number] = ["age", 30];
let record: [number, string, boolean] = [1, "Alice", true];
// Ошибки:
point = [10]; // Ошибка! Нужно 2 элемента
point = [10, 20, 30]; // Ошибка! Максимум 2
point = ["10", 20]; // Ошибка! Первый должен быть number
entry = [30, "age"]; // Ошибка! Порядок типов важен
Доступ к элементам кортежа
const pair: [string, number] = ["age", 30];
const key = pair[0]; // string
const value = pair[1]; // number
// const oob = pair[2]; // Ошибка! Tuple type '[string, number]' has no element at index '2'
// Деструктуризация
const [k, v] = pair;
// k: string, v: number
Именованные кортежи (labeled tuples)
// Имена для документации — не влияют на типы
type Point = [x: number, y: number];
type Entry = [key: string, value: unknown];
type Range = [start: number, end: number];
const p: Point = [10, 20];
// Подсказки в IDE покажут x и y
// Полезно для параметров функций
function plot(...point: [x: number, y: number, z?: number]): void {
// ...
}
plot(1, 2); // OK
plot(1, 2, 3); // OK
Опциональные элементы в кортежах
type Coordinate = [number, number, number?];
const point2D: Coordinate = [10, 20]; // OK
const point3D: Coordinate = [10, 20, 30]; // OK
// Rest-элементы в кортежах
type StringAndNumbers = [string, ...number];
const data: StringAndNumbers = ["scores", 90, 85, 92]; // OK
type StartEnd = [first: string, ...middle: number, last: string];
const se: StartEnd = ["start", 1, 2, 3, "end"]; // OK
Readonly кортежи
const point: readonly [number, number] = [10, 20];
point[0] = 5; // Ошибка! Cannot assign to '0' because it is a read-only property
point.push(30); // Ошибка!
// as const автоматически создаёт readonly tuple с литеральными типами
const rgb = [255, 128, 0] as const;
// Тип: readonly [255, 128, 0]
Variadic Tuple Types (TypeScript 4.0+)
Позволяют работать с кортежами обобщённо:
// Конкатенация кортежей
type Concat<A extends unknown, B extends unknown> = [...A, ...B];
type Result = Concat<[1, 2], [3, 4]>; // [1, 2, 3, 4]
// Добавление элемента в начало
type Prepend<T, Tuple extends unknown> = [T, ...Tuple];
type WithId = Prepend<number, [string, boolean]>; // [number, string, boolean]
// Реальный пример: типизированный partial apply
function partialApply<T extends unknown, U extends unknown, R>(
fn: (...args: [...T, ...U]) => R,
...headArgs: T
): (...tailArgs: U) => R {
return (...tailArgs) => fn(...headArgs, ...tailArgs);
}
function add(a: number, b: number, c: number): number {
return a + b + c;
}
const add10 = partialApply(add, 10);
// (b: number, c: number) => number
const result = add10(20, 30); // 60
Полезные паттерны
Tuple как возвращаемый тип (React-стиль)
function useState<T>(initial: T): [T, (value: T) => void] {
let state = initial;
const setState = (value: T) => {
state = value;
};
return [state, setState];
}
const [count, setCount] = useState(0);
// count: number, setCount: (value: number) => void
Типизация Array методов
const numbers: number = [1, 2, 3, 4, 5];
// map — TS выводит тип результата
const strings = numbers.map(String); // string
const doubled = numbers.map((n) => n * 2); // number
// filter — TS выводит тип, но для сужения нужен type guard
const mixed: (string | number) = [1, "two", 3, "four"];
// Без type guard — тип не сужается
const onlyStrings = mixed.filter((x) => typeof x === "string");
// (string | number) — TS не сужает!
// С type guard — правильное сужение
const onlyStrings = mixed.filter((x): x is string => typeof x === "string");
// string
Частые ошибки
- Пустой массив без аннотации — TS выведет
anyилиnever
const items = ; // any (с noImplicitAny — ошибка)
const items: string = ; // Правильно
- Мутация readonly массива — используйте немутирующие методы
- Кортежи теряют тип при spread — используйте
as const
function getPoint: [number, number] {
return [10, 20];
}
const point = getPoint;
// point: [number, number] — OK, тип сохранён
- Путать массив и кортеж —
numberэто массив любой длины,[number, number]это ровно 2 элемента - Забывать про мутации push/splice — TypeScript разрешает push в кортеж (известная проблема)
const pair: [string, number] = ["age", 30];
pair.push("extra"); // К сожалению, не ошибка! Это "дырка" в системе типов
Практика
- Создайте массив объектов
Userи отфильтруйте с type guard - Напишите функцию, возвращающую кортеж
[error: string | null, data: T | null] - Используйте
as constдля создания union типа из массива значений - Создайте variadic tuple type для конкатенации двух кортежей
- Сделайте readonly массив и попробуйте его изменить
Связанные темы
- Примитивные типы — типы элементов массива
- Generics — обобщённые типы для массивов
- Литеральные типы — as const и литеральные массивы
- Mapped types — трансформация типов элементов