Массивы и кортежи

Типизированные массивы (Array<T>, T) и кортежи (tuple) — фиксированные по длине и типам массивы.

Зачем нужно

  • Массивы — одна из самых частых структур данных
  • Типизация массивов предотвращает ошибки при работе с элементами
  • Кортежи позволяют описать массив с фиксированной структурой (координаты, пары ключ-значение)
  • Readonly-массивы защищают данные от мутации

Где используется

  • Работа с коллекциями данных в любом TypeScript-проекте
  • API-ответы часто содержат массивы объектов
  • React hooks возвращают кортежи: [state, setState]
  • Функции с переменным числом аргументов используют variadic tuples

Предпосылки

Типизация массивов

Два синтаксиса — равнозначны

// Синтаксис 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

Частые ошибки

  1. Пустой массив без аннотации — TS выведет any или never
const items = ; // any (с noImplicitAny — ошибка)
const items: string = ; // Правильно
  1. Мутация readonly массива — используйте немутирующие методы
  2. Кортежи теряют тип при spread — используйте as const
function getPoint: [number, number] {
  return [10, 20];
}
const point = getPoint;
// point: [number, number] — OK, тип сохранён
  1. Путать массив и кортежnumber это массив любой длины, [number, number] это ровно 2 элемента
  2. Забывать про мутации push/splice — TypeScript разрешает push в кортеж (известная проблема)
const pair: [string, number] = ["age", 30];
pair.push("extra"); // К сожалению, не ошибка! Это "дырка" в системе типов

Практика

  1. Создайте массив объектов User и отфильтруйте с type guard
  2. Напишите функцию, возвращающую кортеж [error: string | null, data: T | null]
  3. Используйте as const для создания union типа из массива значений
  4. Создайте variadic tuple type для конкатенации двух кортежей
  5. Сделайте readonly массив и попробуйте его изменить

Связанные темы

Ресурсы