$ sudo teach IT
МОДУЛЬ 3 · УРОК 4

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

Типизация массивов, фиксированные кортежи — как TypeScript помогает работать с коллекциями данных безопасно и удобно

~30 минут Для новичков

1. Что такое массив — аналогия с полкой

Друзья, давайте начнём с самого простого. Представьте, что у вас есть полка в шкафу. На этой полке вы можете хранить книги. Каждая книга стоит рядом с другой — и все они на одной полке. Массив в программировании — это именно такая «полка для данных».

Массив — это упорядоченная коллекция элементов одного типа. Все элементы стоят «рядком», и к каждому можно обратиться по номеру. Нумерация начинается с нуля — это важно запомнить!

Аналогия: Если полка — это массив, то каждая книга на ней — это элемент массива. Книга под номером 0 — первая, под номером 1 — вторая, и так далее.

В JavaScript массивы создаются через квадратные скобки. Вот так:

// Простой массив с числами
const numbers: number[] = [1, 2, 3, 4, 5];

// Массив со строками
const fruits: string[] = ["яблоко", "банан", "вишня"];

// Пустой массив
const emptyArray: number[] = [];

// Массив с булевыми значениями
const flags: boolean[] = [true, false, true, true];

// Обращение к элементу по индексу
console.log(numbers[0]); // 1 (первый элемент)
console.log(fruits[1]);  // "банан" (второй элемент)

// Длина массива
console.log(numbers.length); // 5

Здесь : number[] — это аннотация типа, которая говорит TypeScript: «эта переменная — массив, и в ней могут быть только числа».

Давайте посмотрим на это подробнее. Если вы попытаетесь положить строку в массив чисел, TypeScript сразу скажет, что это ошибка:

// numbers — массив чисел
const numbers: number[] = [1, 2, 3];

// Это будет ошибкой!
numbers.push("привет"); // Ошибка: тип "string" не может быть назначен типу "number"
numbers.push(true);     // Ошибка: тип "boolean" не может быть назначен типу "number"

// А это работает нормально
numbers.push(4);        // OK
numbers.push(100);      // OK

Почему это важно? В обычном JavaScript вы можете случайно добавить в массив что угодно — строку, объект, null, undefined. Это приводит к трудноуловимым багам. TypeScript защищает вас от этого!

Давайте рассмотрим несколько практических примеров, чтобы вы лучше поняли, как работают массивы:

// Список покупок
const shoppingList: string[] = [
  "молоко",
  "хлеб",
  "сыр",
  "яйца",
  "масло"
];

// Оценки учеников
const grades: number[] = [5, 4, 5, 3, 5, 4, 4, 5];

// Результаты тестов (прошёл/не прошёл)
const testResults: boolean[] = [true, true, false, true, true, false];

// Выведем каждый элемент
for (let i = 0; i < shoppingList.length; i++) {
  console.log(`${i + 1}. ${shoppingList[i]}`);
}
// Вывод:
// 1. молоко
// 2. хлеб
// 3. сыр
// 4. яйца
// 5. масло

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

2. Типизация массивов: Type[] и Array<Type>

В TypeScript есть два способа указать, что переменная — это массив определённого типа. Оба способа абсолютно равнозначны и работают одинаково. Выбирайте тот, который вам больше нравится!

Способ 1: Синтаксис с квадратными скобками (Type[])

Это самый распространённый и краткий способ. Вы пишете тип, а потом квадратные скобки:

// Массив чисел — способ 1
const scores: number[] = [10, 20, 30];

// Массив строк — способ 1
const names: string[] = ["Анна", "Борис", "Виктор"];

// Массив булевых — способ 1
const isActive: boolean[] = [true, false, true];

Способ 2: Обобщённый синтаксис (Array<Type>)

Этот способ использует обобщения (generics) — специальный механизм TypeScript. Здесь тип передаётся в угловых скобках:

// Массив чисел — способ 2
const scores: Array<number> = [10, 20, 30];

// Массив строк — способ 2
const names: Array<string> = ["Анна", "Борис", "Виктор"];

// Массив булевых — способ 2
const isActive: Array<boolean> = [true, false, true];

Как видите, оба способа дают одинаковый результат. Разница только в синтаксисе:

number[] — короткий, современный, чаще используется

Array<number> — более «явный», иногда удобнее для чтения

Рекомендация: большинство разработчиков предпочитают number[],потому что это короче.

Давайте рассмотрим более сложные примеры, гдеэти дваспособвозможнонепроявляет преимущество:

// Массив объектов — когда полезен Array
interface User {
  name: string;
  age: number;
}

// Оба варианта работают:
const users1: User[] = [
  { name: "Анна", age: 25 },
  { name: "Борис", age: 30 }
];

const users2: Array<User> = [
  { name: "Анна", age: 25 },
  { name: "Борис", age: 30 }
];

// Вложенные массивы (массив массивов)
const matrix1: number[][] = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9]
];

const matrix2: Array<Array<number>> = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9]
];

// Массив кортежей (пока просто посмотрите, позже разберём!)
const coordinates1: [number, number][] = [
  [10, 20],
  [30, 40],
  [50, 60]
];

const coordinates2: Array<[number, number]> = [
  [10, 20],
  [30, 40],
  [50, 60]
];

Совет: Используйте Type[] по умолчанию. Переключайтесь на Array<Type> только когда это делает код читаемее (например, в сложных вложенных типах).

Теперь давайте посмотрим, как работают методы массивов с типизацией. TypeScript «знает» тип элементов и автоматически определяет тип результата:

// Метод map — возвращает НОВЫЙ массив
const numbers: number[] = [1, 2, 3, 4, 5];

// TypeScript автоматически понимает, что результат — массив строк
const doubled = numbers.map(n => n * 2);        // number[]
const asStrings = numbers.map(n => `Число: ${n}`); // string[]

console.log(doubled);     // [2, 4, 6, 8, 10]
console.log(asStrings);   // ["Число: 1", "Число: 2", ...]

// Метод filter — возвращает новый массив с тем же типом
const evens = numbers.filter(n => n % 2 === 0); // number[]
console.log(evens); // [2, 4]

// Метод find — возвращает один элемент или undefined
const found = numbers.find(n => n > 3); // number | undefined
console.log(found); // 4

// Метод reduce — возвращает результат любого типа
const sum = numbers.reduce((acc, n) => acc + n, 0); // number
console.log(sum); // 15

// Метод some / every — возвращают boolean
const hasEven = numbers.some(n => n % 2 === 0);  // boolean
const allPositive = numbers.every(n => n > 0);    // boolean
console.log(hasEven);   // true
console.log(allPositive); // true

3. Типизация смешанных массивов

Иногда вам нужно хранить в массиве данные разных типов. Например, вы хотите создать массив, в котором лежат и числа, и строки. TypeScript позволяет это сделать с помощью объединённых типов (union types).

Объединение типов (Union Types)

Чтобы указать, что массив может содержать элементы нескольких типов, используют символ вертикальной черты |:

// Массив, который может содержать числа ИЛИ строки
const mixed: (number | string)[] = [1, "два", 3, "четыре"];

// Массив, который может содержать числа, строки ИЛИ булевы
const multiType: (number | string | boolean)[] = [1, "привет", true, 42];

// Массив с null и undefined
const withNull: (string | null)[] = ["привет", null, "мир"];
const withUndefined: (number | undefined)[] = [1, undefined, 3];

// Массив с любым типом (не рекомендуется!)
const anyArray: any[] = [1, "строка", true, { name: "объект" }];

Осторожно! Использование any[] отключает типизацию для этого массива. Это противоречит философии TypeScript. Старайтесь использовать конкретные типы!

Когда вы обращаетесь к элементу смешанного массива, TypeScript «не знает», какой именно тип у этого элемента. Поэтому вам нужно проверять тип перед использованием:

// Смешанный массив
const mixed: (number | string)[] = [1, "два", 3, "четыре"];

// TypeScript НЕ позволяет делать так:
// console.log(mixed[0].toUpperCase()); // Ошибка! number не имеет toUpperCase

// Нужно сначала проверить тип
const firstElement = mixed[0];

if (typeof firstElement === "string") {
  console.log(firstElement.toUpperCase()); // OK! TypeScript "знает", что это строка
} else {
  console.log(firstElement.toFixed(2)); // OK! TypeScript "знает", что это число
}

// Или использовать приведение типов
const secondElement = mixed[1] as string;
console.log(secondElement.toUpperCase()); // "ДВА"

// Можно перебрать массив и обработать каждый тип
mixed.forEach(item => {
  if (typeof item === "number") {
    console.log(`Число: ${item * 2}`);
  } else {
    console.log(`Строка: ${item.toUpperCase()}`);
  }
});

Практический пример: массив данных формы

Давайте создадим более реалистичный пример — массив полей формы, где каждое поле может быть строкой, числом или булевым значением:

// Тип для значения поля формы
type FormValue = string | number | boolean;

// Массив полей формы
const formData: FormValue[] = [
  "Иван",           // имя
  25,               // возраст
  true,             // согласие на обработку данных
  "ivan@mail.ru",   // email
  180               // рост в см
];

// Функция для вывода данных формы
function printFormData(data: FormValue[]): void {
  data.forEach((value, index) => {
    const type = typeof value;
    console.log(`Поле ${index + 1} (${type}): ${value}`);
  });
}

printFormData(formData);
// Вывод:
// Поле 1 (string): Иван
// Поле 2 (number): 25
// Поле 3 (boolean): true
// Поле 4 (string): ivan@mail.ru
// Поле 5 (number): 180

Массив объектов разных типов

Часто вам нужно хранить в массиве объекты разных структур. Это можно сделать с помощью объединения интерфейсов:

// Разные типы фигур
interface Circle {
  kind: "circle";
  radius: number;
}

interface Rectangle {
  kind: "rectangle";
  width: number;
  height: number;
}

interface Triangle {
  kind: "triangle";
  base: number;
  height: number;
}

// Массив разных фигур
const shapes: (Circle | Rectangle | Triangle)[] = [
  { kind: "circle", radius: 5 },
  { kind: "rectangle", width: 10, height: 20 },
  { kind: "triangle", base: 8, height: 12 },
  { kind: "circle", radius: 3 },
  { kind: "rectangle", width: 5, height: 8 }
];

// Функция для вычисления площади
function calculateArea(shape: Circle | Rectangle | Triangle): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "rectangle":
      return shape.width * shape.height;
    case "triangle":
      return (shape.base * shape.height) / 2;
  }
}

// Выведем площади всех фигур
shapes.forEach((shape, index) => {
  const area = calculateArea(shape);
  console.log(`Фигура ${index + 1} (${shape.kind}): площадь = ${area.toFixed(2)}`);
});

4. Readonly массивы

Иногда вы хотите создать массив, который нельзя изменить после создания. Это как «заклеить» полку клейкой лентой — ничего нельзя добавить, удалить или переместить. В TypeScript для этого есть модификатор readonly.

// Обычный массив — можно изменять
const mutableArray: number[] = [1, 2, 3];
mutableArray.push(4);      // OK
mutableArray[0] = 100;     // OK
mutableArray.pop();        // OK

// Readonly массив — нельзя изменять!
const readonlyArray: readonly number[] = [1, 2, 3];

// Все эти операции вызовут ошибку:
// readonlyArray.push(4);      // Ошибка! Метод "push" не существует в типе "readonly number[]"
// readonlyArray[0] = 100;     // Ошибка! Индексная присваиваемость запрещена
// readonlyArray.pop();        // Ошибка! Метод "pop" не существует
// readonlyArray.splice(0, 1); // Ошибка! Метод "splice" не существует

// Но ЧИТАТЬ можно:
console.log(readonlyArray[0]);   // 1
console.log(readonlyArray.length); // 3

// Можно перебирать
readonlyArray.forEach(item => console.log(item)); // OK

// Можно использовать методы, которые не изменяют массив:
const doubled = readonlyArray.map(n => n * 2);     // OK, возвращает новый массив
const sum = readonlyArray.reduce((acc, n) => acc + n, 0); // OK

Альтернативный синтаксис: ReadonlyArray<Type>

Есть ещё один способ создать readonly массив — с помощью обобщённого типа:

// Тот же самый readonly массив, другой синтаксис
const readonlyArray2: ReadonlyArray<number> = [1, 2, 3];

// Аналогичные ошибки:
// readonlyArray2.push(4);  // Ошибка!
// readonlyArray2[0] = 10;  // Ошибка!

// Константные массивы автоматически readonly
const constArray = [1, 2, 3] as const;
// TypeScript автоматически делает его readonly number[]
// constArray.push(4); // Ошибка!

Когда использовать readonly массивы?

// 1. Константы — данные, которые не должны меняться
const DAYS_OF_WEEK: readonly string[] = [
  "Понедельник", "Вторник", "Среда",
  "Четверг", "Пятница", "Суббота", "Воскресенье"
];

// 2. Конфигурации — настройки, которые не меняются
const ALLOWED_ORIGINS: readonly string[] = [
  "https://example.com",
  "https://app.example.com"
];

// 3. Значения перечислений (enum-подобные константы)
const HTTP_STATUS: readonly number[] = [200, 201, 204, 301, 302, 400, 401, 403, 404, 500];

// 4. Когда передаёте массив в функцию и хотите гарантировать,
//    что функция его не изменит
function sumArray(numbers: readonly number[]): number {
  return numbers.reduce((acc, n) => acc + n, 0);
  // Функция не может изменить numbers — это безопасно!
}

const scores = [10, 20, 30];
console.log(sumArray(scores)); // 60

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

5. Типизация пустых массивов

Пустой массив — это частый случай. Вы создаёте пустой массив, а потом постепенно наполняете его данными. Но какой тип у пустого массива? TypeScript может вывести это автоматически, но иногда ему нужна ваша помощь.

// TypeScript автоматически понимает тип
const numbers: number[] = [];     // пустой массив чисел
numbers.push(1);                  // OK
numbers.push(2);                  // OK
// numbers.push("три");           // Ошибка! Ожидается number

const strings: string[] = [];     // пустой массив строк
strings.push("привет");           // OK
// strings.push(42);              // Ошибка! Ожидается string

// Если не указать тип, TypeScript выведет any[]
const ambiguous = [];             // any[]
ambiguous.push(1);                // OK
ambiguous.push("два");            // OK — потому что any[]
ambiguous.push(true);             // OK — потому что any[]
// Это опасно! Вы потеряли типизацию.

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

Практические примеры пустых массивов

// Пример 1: Список задач
interface Task {
  id: number;
  title: string;
  completed: boolean;
}

const tasks: Task[] = [];
tasks.push({ id: 1, title: "Купить продукты", completed: false });
tasks.push({ id: 2, title: "Позвонить маме", completed: true });

// Пример 2: Лог ошибок
interface ErrorLog {
  timestamp: Date;
  message: string;
  level: "info" | "warning" | "error";
}

const errorLogs: ErrorLog[] = [];
errorLogs.push({
  timestamp: new Date(),
  message: "Сервер не отвечает",
  level: "error"
});

// Пример 3: Результаты вычислений
const results: number[] = [];
for (let i = 1; i <= 10; i++) {
  results.push(i ** 2);
}
console.log(results); // [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

// Пример 4: Коллекция пользователей
interface User {
  name: string;
  email: string;
}

const users: User[] = [];
// Наполняем данные из какого-то источника
const rawData = [
  { name: "Анна", email: "anna@mail.com" },
  { name: "Борис", email: "boris@mail.com" }
];

rawData.forEach(item => {
  users.push({ name: item.name, email: item.email });
});

console.log(users); // [{ name: "Анна", email: "anna@mail.com" }, ...]

6. Что такое кортеж (tuple)

Теперь давайте перейдём к кортежам — это особый вид массивов, который имеет фиксированную длину и фиксированные типы для каждого элемента.

Представьте обычный массив как полку, на которой стоят книги. А кортеж — это как шахматная доска: ровно 8×8 клеток, и в каждой клетке может быть только определённая фигура.

Аналогия: Обычный массив — это список покупок (может содержать что угодно, длина может меняться). Кортеж — это рецепт (точный набор ингредиентов в определённом порядке).

Массив vs Кортеж: ключевые отличия

Обычный массив (number[]):

  • Длина может быть любой
  • Все элементы одного типа
  • Можно добавлять и удалять элементы
  • Нельзя знать точный тип каждого элемента по позиции

Кортеж ([number, string]):

  • Длина фиксирована
  • Каждый элемент имеет свой тип
  • Нельзя добавлять или удалять элементы
  • Тип каждого элемента известен по позиции

7. Синтаксис кортежей

Синтаксис кортежей похож на обычный массив, но типы перечисляются для каждого элемента отдельно:

// Кортеж из двух элементов: строка и число
const person: [string, number] = ["Анна", 25];

// Кортеж из трёх элементов
const record: [string, number, boolean] = ["Запись", 42, true];

// Кортеж с более сложными типами
const point: [number, number, number] = [10, 20, 30];

// Кортеж с необязательными элементами
const optionalTuple: [string, number?] = ["Анна"]; // OK
const optionalTuple2: [string, number?] = ["Анна", 25]; // OK

// Кортеж с rest-элементом (rest tuple)
const restTuple: [string, ...number[]] = ["Анна", 1, 2, 3, 4];

Обращение к элементам кортежа

// Кортеж: [имя, возраст, email]
const user: [string, number, string] = ["Анна", 25, "anna@mail.com"];

// TypeScript "знает" тип каждого элемента:
const name: string = user[0];      // OK! Это строка
const age: number = user[1];       // OK! Это число
const email: string = user[2];     // OK! Это строка

// Можно обращаться по индексу:
console.log(user[0]); // "Анна"
console.log(user[1]); // 25
console.log(user[2]); // "anna@mail.com"

// НО! Нельзя обращаться по несуществующему индексу:
// console.log(user[3]); // Ошибка! Индекс 3 не существует в типе "[string, number, string]"

Нельзя добавлять или удалять элементы

const point: [number, number] = [10, 20];

// Все эти операции вызовут ошибку:
// point.push(30);       // Ошибка! Метод "push" не существует в типе "[number, number]"
// point.pop();          // Ошибка! Метод "pop" не существует
// point.splice(0, 1);   // Ошибка! Метод "splice" не существует
// point[2] = 30;        // Ошибка! Индекс 2 не существует

// Но МОЖНО изменять существующие элементы:
point[0] = 15;  // OK! Заменяем первый элемент
point[1] = 25;  // OK! Заменяем второй элемент
console.log(point); // [15, 25]

Важно! Кортежи — это фиксированные структуры. Если вам нужно добавлять элементы, используйте обычный массив. Если вам нужна строгая структура — кортеж.

Практические примеры кортежей

// Пример 1: Координаты точки на карте
const coordinates: [number, number] = [55.7558, 37.6173]; // Москва

// Пример 2: Результат функции (успех, данные)
function divide(a: number, b: number): [boolean, number] {
  if (b === 0) {
    return [false, 0];
  }
  return [true, a / b];
}

const [success, result] = divide(10, 2);
console.log(`Успех: ${success}, Результат: ${result}`); // Успех: true, Результат: 5

// Пример 3: Запись в логе
type LogEntry = [Date, string, "info" | "warning" | "error"];

const logs: LogEntry[] = [
  [new Date(), "Сервер запущен", "info"],
  [new Date(), "Мало памяти", "warning"],
  [new Date(), "Ошибка подключения", "error"]
];

// Пример 4: Строка-значение (key-value)
type KeyValue = [string, any];

const settings: KeyValue[] = [
  ["theme", "dark"],
  ["language", "ru"],
  ["fontSize", 14],
  ["notifications", true]
];

// Пример 5: Диапазон значений
type Range = [number, number];

const ageRange: Range = [0, 120];
const temperatureRange: Range = [-50, 50];
const percentageRange: Range = [0, 100];

Кортеж vs Интерфейс: что выбрать?

// Вариант 1: Кортеж
type UserTuple = [string, number, string];
const user1: UserTuple = ["Анна", 25, "anna@mail.com"];

// Вариант 2: Интерфейс
interface UserInterface {
  name: string;
  age: number;
  email: string;
}
const user2: UserInterface = {
  name: "Борис",
  age: 30,
  email: "boris@mail.com"
};

// Кортеж удобнее, когда:
// - Порядок элементов важен и фиксирован
// - Элементы разного типа
// - Нужна компактная запись

// Интерфейс удобнее, когда:
// - Имена элементов важнее их порядка
// - Элементов много
// - Нужно добавлять/удалять свойства

8. Readonly кортежи

Readonly кортежи — это кортежи, которые нельзя изменить после создания. Это как «заклеить» рецепт клейкой лентой — вы можете его читать, но не можете менять.

// Обычный кортеж — можно изменять элементы
const mutableTuple: [string, number] = ["Анна", 25];
mutableTuple[0] = "Борис";  // OK!
mutableTuple[1] = 30;       // OK!

// Readonly кортеж — нельзя изменять!
const readonlyTuple: readonly [string, number] = ["Анна", 25];

// Эти операции вызовут ошибку:
// readonlyTuple[0] = "Борис";  // Ошибка! Индексная присваиваемость запрещена
// readonlyTuple[1] = 30;       // Ошибка! Индексная присваиваемость запрещена
// readonlyTuple.push("ещё");   // Ошибка! Метод "push" не существует

// Но ЧИТАТЬ можно:
console.log(readonlyTuple[0]); // "Анна"
console.log(readonlyTuple[1]); // 25

// Можно деструктуризировать:
const [name, age] = readonlyTuple;
console.log(name, age); // "Анна" 25

Readonly кортежи с const

// Использование "as const" создаёт readonly кортеж
const coordinates = [55.7558, 37.6173] as const;
// TypeScript понимает это как readonly [number, number]

// Нельзя изменить:
// coordinates[0] = 0; // Ошибка!

// Но можно использовать в функциях:
function getDistance(
  point1: readonly [number, number],
  point2: readonly [number, number]
): number {
  const dx = point2[0] - point1[0];
  const dy = point2[1] - point1[1];
  return Math.sqrt(dx ** 2 + dy ** 2);
}

const moscow: readonly [number, number] = [55.7558, 37.6173];
const spb: readonly [number, number] = [59.9343, 30.3351];
console.log(getDistance(moscow, spb)); // ~634 км

Практический пример: неизменяемые данные

// Конфигурация приложения — не должна меняться
const AppConfig: readonly [string, number, boolean] = [
  "MyApp",      // название
  1,            // версия
  true          // режим отладки
];

// Цвета темы — константы
const ThemeColors: readonly [string, string, string] = [
  "#ffffff",  // фон
  "#000000",  // текст
  "#3595fd"   // акцент
];

// Результаты вычислений, которые не должны изменяться
const Statistics: readonly [number, number, number, number] = [
  100,  // всего пользователей
  85,   // активные
  10,   // неактивные
  5     // заблокированные
];

// Функция, которая принимает readonly кортеж
function printUser(
  user: readonly [string, number, string]
): void {
  console.log(`Имя: ${user[0]}`);
  console.log(`Возраст: ${user[1]}`);
  console.log(`Email: ${user[2]}`);
}

printUser(["Анна", 25, "anna@mail.com"]);

9. Когда использовать кортежи

Кортежи полезны во многих ситуациях. Давайте рассмотрим основные случаи, когда они gerçekten удобны:

1. Возврат нескольких значений из функции

// Функция возвращает [успех, результат]
function divide(a: number, b: number): [boolean, number] {
  if (b === 0) {
    return [false, 0];
  }
  return [true, a / b];
}

const [success, result] = divide(10, 2);
if (success) {
  console.log(`Результат: ${result}`); // Результат: 5
} else {
  console.log("Деление на ноль!");
}

// Функция возвращает [минимум, максимум]
function getMinMax(numbers: number[]): [number, number] {
  const min = Math.min(...numbers);
  const max = Math.max(...numbers);
  return [min, max];
}

const [min, max] = getMinMax([3, 1, 4, 1, 5, 9, 2, 6]);
console.log(`От ${min} до ${max}`); // От 1 до 9

// Функция возвращает [код ошибки, сообщение]
function processRequest(url: string): [number, string] {
  if (url.startsWith("http")) {
    return [200, "OK"];
  }
  return [400, "Неверный URL"];
}

const [code, message] = processRequest("https://example.com");
console.log(`${code}: ${message}`); // 200: OK

2. Пары ключ-значение

// Настройки в формате [ключ, значение]
type Setting = [string, string | number | boolean];

const settings: Setting[] = [
  ["theme", "dark"],
  ["language", "ru"],
  ["fontSize", 14],
  ["notifications", true],
  ["autoSave", false]
];

// Функция для поиска настройки
function findSetting(
  settings: Setting[],
  key: string
): Setting | undefined {
  return settings.find(([k]) => k === key);
}

const themeSetting = findSetting(settings, "theme");
if (themeSetting) {
  console.log(`Тема: ${themeSetting[1]}`); // Тема: dark
}

// Объект настроек
type Config = [string, any][];
const config: Config = [
  ["apiUrl", "https://api.example.com"],
  ["timeout", 5000],
  ["retries", 3],
  ["debug", false]
];

// Преобразуем в объект
function configToObject(config: Config): Record<string, any> {
  const result: Record<string, any> = {};
  config.forEach(([key, value]) => {
    result[key] = value;
  });
  return result;
}

console.log(configToObject(config));
// { apiUrl: "https://api.example.com", timeout: 5000, ... }

3. Координаты и геометрические данные

// Точка в 2D пространстве
type Point2D = [number, number];

// Точка в 3D пространстве
type Point3D = [number, number, number];

// Прямоугольник: [x, y, width, height]
type Rectangle = [number, number, number, number];

// Функция для вычисления расстояния
function distance(p1: Point2D, p2: Point2D): number {
  const dx = p2[0] - p1[0];
  const dy = p2[1] - p1[1];
  return Math.sqrt(dx * dx + dy * dy);
}

const pointA: Point2D = [0, 0];
const pointB: Point2D = [3, 4];
console.log(distance(pointA, pointB)); // 5

// Функция для проверки, находится ли точка в прямоугольнике
function isPointInRect(
  point: Point2D,
  rect: Rectangle
): boolean {
  const [px, py] = point;
  const [rx, ry, rw, rh] = rect;
  return px >= rx && px <= rx + rw && py >= ry && py <= ry + rh;
}

const myPoint: Point2D = [5, 5];
const myRect: Rectangle = [0, 0, 10, 10];
console.log(isPointInRect(myPoint, myRect)); // true

4. История изменений

// Запись об изменении: [время, старое_значение, новое_значение]
type ChangeRecord = [Date, any, any];

const history: ChangeRecord[] = [
  [new Date("2024-01-01"), null, "Анна"],
  [new Date("2024-01-05"), "Анна", "Анна Иванова"],
  [new Date("2024-01-10"), "Анна Иванова", "Анна Петрова"]
];

// Функция для вывода истории
function printHistory(records: ChangeRecord[]): void {
  records.forEach(([date, oldVal, newVal]) => {
    console.log(`${date.toISOString().split("T")[0]}: ${oldVal} → ${newVal}`);
  });
}

printHistory(history);
// 2024-01-01: null → Анна
// 2024-01-05: Анна → Анна Иванова
// 2024-01-10: Анна Иванова → Анна Петрова

5. Результаты валидации

// Результат валидации: [валидно, сообщение_об_ошибке]
type ValidationResult = [boolean, string];

function validateEmail(email: string): ValidationResult {
  if (!email.includes("@")) {
    return [false, "Email должен содержать @"];
  }
  if (!email.includes(".")) {
    return [false, "Email должен содержать точку"];
  }
  return [true, "OK"];
}

function validateAge(age: number): ValidationResult {
  if (age < 0) {
    return [false, "Возраст не может быть отрицательным"];
  }
  if (age > 150) {
    return [false, "Возраст слишком большой"];
  }
  return [true, "OK"];
}

const emailResult = validateEmail("user@example.com");
if (!emailResult[0]) {
  console.log(`Ошибка: ${emailResult[1]}`);
}

const ageResult = validateAge(25);
if (ageResult[0]) {
  console.log("Возраст валиден!");
}

10. Деструктуризация

Деструктуризация — это способ «разобрать» массив на отдельные переменные. Это очень удобно, особенно с кортежами!

Деструктуризация обычных массивов

// Обычный массив
const numbers = [10, 20, 30, 40, 50];

// Деструктуризация — извлекаем первые элементы
const [first, second, third] = numbers;
console.log(first);  // 10
console.log(second); // 20
console.log(third);  // 30

// Пропускаем элементы через запятую
const [, , thirdOnly] = numbers;
console.log(thirdOnly); // 30

// Используем rest-оператор для сбора остатка
const [head, ...tail] = numbers;
console.log(head); // 10
console.log(tail); // [20, 30, 40, 50]

// Значения по умолчанию
const [a = 0, b = 0, c = 0, d = 0, e = 0, f = 0] = numbers;
console.log(f); // 0 (элемента нет — используется значение по умолчанию)

// Пример с функцией
function getUser(): [string, number, string] {
  return ["Анна", 25, "anna@mail.com"];
}

const [userName, userAge, userEmail] = getUser();
console.log(`${userName}, ${userAge} лет, ${userEmail}`);
// Анна, 25 лет, anna@mail.com

Деструктуризация кортежей

// Кортеж с именованными переменными — читаемо и понятно
type Person = [name: string, age: number, email: string];

const person: Person = ["Анна", 25, "anna@mail.com"];

// Деструктуризация с именами (TypeScript 4.0+)
const [personName, personAge, personEmail] = person;
console.log(personName);  // "Анна"
console.log(personAge);   // 25
console.log(personEmail); // "anna@mail.com"

// Именованные элементы кортежа (TypeScript 4.0+)
type NamedTuple = [name: string, age: number];

const user: NamedTuple = ["Борис", 30];

// При деструктуризации имена не используются напрямую,
// но помогают при чтении типа:
function processUser([name, age]: NamedTuple): void {
  console.log(`${name} (${age} лет)`);
}

processUser(user); // "Борис (30 лет)"

// Деструктуризация в цикле
const coordinates: [number, number][] = [
  [10, 20],
  [30, 40],
  [50, 60]
];

coordinates.forEach(([x, y]) => {
  console.log(`x: ${x}, y: ${y}`);
});
// x: 10, y: 20
// x: 30, y: 40
// x: 50, y: 60

Деструктуризация с переименованием

// Переименование при деструктуризации
const tuple: [string, number] = ["Анна", 25];

// Без переименования
const [name1, age1] = tuple;

// С переименованием (используем "as")
// Но для массивов переименование работает иначе — через промежуточную переменную
const [tempName, tempAge] = tuple;
const userName2 = tempName;
const userAge2 = tempAge;

// Более чистый способ — деструктуризация в параметрах
function greet([personName, personAge]: [string, number]): void {
  console.log(`Привет, ${personName}! Тебе ${personAge} лет.`);
}

greet(["Виктор", 35]); // "Привет, Виктор! Тебе 35 лет."

// Деструктуризация с rest-оператором
const mixed: [string, number, ...boolean[]] = ["Анна", 25, true, false, true];
const [mName, mAge, ...mFlags] = mixed;
console.log(mName);  // "Анна"
console.log(mAge);   // 25
console.log(mFlags); // [true, false, true]

Практический пример: обработка данных

// Данные студентов: [имя, оценки...]
type Student = [string, ...number[]];

const students: Student[] = [
  ["Анна", 5, 4, 5, 5, 4],
  ["Борис", 3, 4, 3, 4, 3],
  ["Виктор", 5, 5, 5, 5, 5],
  ["Галина", 4, 4, 4, 4, 5]
];

// Функция для вычисления средней оценки
function getAverage(student: Student): number {
  const [name, ...grades] = student;
  const sum = grades.reduce((acc, g) => acc + g, 0);
  return sum / grades.length;
}

// Выведем средние оценки
students.forEach(student => {
  const [name, ...grades] = student;
  const average = getAverage(student);
  console.log(`${name}: средняя оценка ${average.toFixed(2)}`);
});
// Анна: средняя оценка 4.60
// Борис: средняя оценка 3.40
// Виктор: средняя оценка 5.00
// Галина: средняя оценка 4.20

// Функция для поиска лучшего студента
function findBestStudent(students: Student[]): Student {
  return students.reduce((best, current) => {
    return getAverage(current) > getAverage(best) ? current : best;
  });
}

const best = findBestStudent(students);
console.log(`Лучший студент: ${best[0]}`); // "Лучший студент: Виктор"

11. Массивы в функциях

Массивы и кортежи часто используются как параметры и возвращаемые значения функций. Давайте рассмотрим основные паттерны:

Функции, принимающие массивы

// Функция для суммы чисел в массиве
function sumArray(numbers: number[]): number {
  return numbers.reduce((acc, n) => acc + n, 0);
}

console.log(sumArray([1, 2, 3, 4, 5])); // 15

// Функция для фильтрации уникальных значений
function unique<T>(array: T[]): T[] {
  return [...new Set(array)];
}

console.log(unique([1, 2, 2, 3, 3, 3])); // [1, 2, 3]
console.log(unique(["а", "б", "а", "в"])); // ["а", "б", "в"]

// Функция для.chunk (разбиение на группы)
function chunk<T>(array: T[], size: number): T[][] {
  const chunks: T[][] = [];
  for (let i = 0; i < array.length; i += size) {
    chunks.push(array.slice(i, i + size));
  }
  return chunks;
}

console.log(chunk([1, 2, 3, 4, 5, 6, 7], 3));
// [[1, 2, 3], [4, 5, 6], [7]]

// Функция для поиска в массиве
function findByProperty<T, K extends keyof T>(
  array: T[],
  property: K,
  value: T[K]
): T | undefined {
  return array.find(item => item[property] === value);
}

interface User {
  name: string;
  age: number;
}

const users: User[] = [
  { name: "Анна", age: 25 },
  { name: "Борис", age: 30 }
];

const found = findByProperty(users, "name", "Борис");
console.log(found); // { name: "Борис", age: 30 }

Функции, возвращающие кортежи

// Функция возвращает [минимум, максимум, среднее]
function getStats(numbers: number[]): [number, number, number] {
  const min = Math.min(...numbers);
  const max = Math.max(...numbers);
  const avg = numbers.reduce((a, b) => a + b, 0) / numbers.length;
  return [min, max, avg];
}

const [min, max, avg] = getStats([3, 1, 4, 1, 5, 9, 2, 6]);
console.log(`Мин: ${min}, Макс: ${max}, Среднее: ${avg.toFixed(2)}`);
// Мин: 1, Макс: 9, Среднее: 3.88

// Функция возвращает [успех, данные, сообщение об ошибке]
function fetchData<T>(
  url: string
): [boolean, T | null, string] {
  // Имитация запроса
  if (url === "error") {
    return [false, null, "Ошибка сети"];
  }
  return [true, { data: "данные" } as T, ""];
}

const [success, data, error] = fetchData<{ data: string }>("https://api.example.com");
if (success) {
  console.log(data?.data); // "данные"
} else {
  console.log(error); // "Ошибка сети"
}

// Функция возвращает кортеж с деструктуризацией в параметрах
function processCoordinates(
  [x, y]: [number, number],
  [dx, dy]: [number, number]
): [number, number] {
  return [x + dx, y + dy];
}

const newPos = processCoordinates([10, 20], [5, -3]);
console.log(newPos); // [15, 17]

Rest-параметры и кортежи

// Rest-параметры позволяют принимать любое количество аргументов
function sum(...numbers: number[]): number {
  return numbers.reduce((acc, n) => acc + n, 0);
}

console.log(sum(1, 2, 3));        // 6
console.log(sum(1, 2, 3, 4, 5)); // 15
console.log(sum());               // 0

// Функция с mix параметрами
function log(level: string, ...messages: string[]): void {
  console.log(`[${level}]`, messages.join(" "));
}

log("INFO", "Сервер", "запущен", "на", "порту", "3000");
// [INFO] Сервер запущен на порту 3000

// Rest-параметры с кортежами
function createCoordinates(
  x: number,
  y: number,
  ...rest: number[]
): [number, number, ...number[]] {
  return [x, y, ...rest];
}

const point3D = createCoordinates(10, 20, 30);
console.log(point3D); // [10, 20, 30]

const point4D = createCoordinates(10, 20, 30, 40);
console.log(point4D); // [10, 20, 30, 40]

Callback-функции с массивами

// Функция higher-order, принимающая callback
function applyToArray<T, R>(
  array: T[],
  callback: (item: T, index: number) => R
): R[] {
  return array.map(callback);
}

const numbers = [1, 2, 3, 4, 5];

const doubled = applyToArray(numbers, (n, i) => n * 2);
const indexed = applyToArray(numbers, (n, i) => `${i}: ${n}`);

console.log(doubled);  // [2, 4, 6, 8, 10]
console.log(indexed);  // ["0: 1", "1: 2", "2: 3", "3: 4", "4: 5"]

// Функция для группировки
function groupBy<T, K extends string>(
  array: T[],
  keyFn: (item: T) => K
): Record<K, T[]> {
  const result = {} as Record<K, T[]>;
  array.forEach(item => {
    const key = keyFn(item);
    if (!result[key]) {
      result[key] = [];
    }
    result[key].push(item);
  });
  return result;
}

interface Student {
  name: string;
  grade: number;
}

const students: Student[] = [
  { name: "Анна", grade: 5 },
  { name: "Борис", grade: 4 },
  { name: "Виктор", grade: 5 },
  { name: "Галина", grade: 4 },
  { name: "Дмитрий", grade: 3 }
];

const grouped = groupBy(students, s => s.grade.toString());
console.log(grouped);
// { "5": [{ name: "Анна", ... }, { name: "Виктор", ... }], ... }

12. Ошибки новичков

Давайте рассмотримсамые частые ошибки, которые делают начинающие при работе с массивами и кортежами:

Ошибка 1: Смешивание типов без объединения

// ОШИБКА: Пытаемся положить строку в массив чисел
const numbers: number[] = [1, 2, 3];
// numbers.push("четыре"); // Ошибка!

// ПРАВИЛЬНО: Используем объединение типов
const mixed: (number | string)[] = [1, 2, "три"];

// Или создаём отдельный тип
type NumberOrString = number | string;
const mixed2: NumberOrString[] = [1, "два", 3];

Ошибка 2: Путаница между массивом и кортежем

// Это МАССИВ — можно добавлять элементы
const array: number[] = [1, 2, 3];
array.push(4); // OK!

// Это КОРТЕЖ — нельзя добавлять элементы
const tuple: [number, number, number] = [1, 2, 3];
// tuple.push(4); // Ошибка!

// Типичная ошибка: пытаемся добавить в кортеж
type Point = [number, number];
// const point: Point = [10, 20];
// point.push(30); // Ошибка! Метод "push" не существует

// ПРАВИЛЬНО: Если нужна изменяемая длина — используйте массив
type PointArray = [number, number, ...number[]];
const point: PointArray = [10, 20];
// point.push(30); // ОК!

Ошибка 3: Неправильный тип индекса

// ОШИБКА: Пытаемся обратиться по несуществующему индексу
const tuple: [string, number] = ["Анна", 25];
// const third = tuple[2]; // Ошибка! Индекс 2 не существует

// ПРАВИЛЬНО: Используйте только доступные индексы
const name = tuple[0]; // OK
const age = tuple[1];  // OK

// Если нужен доступ к произвольному индексу — используйте массив
const array: (string | number)[] = ["Анна", 25];
const item = array[2]; // OK, но тип будет string | number | undefined

Ошибка 4: Забыть указать тип пустого массива

// ОШИБКА: Пустой массив без типа — TypeScript выведет any[]
const items = [];      // any[] — опасно!
items.push(1);         // OK
items.push("два");     // OK — потеря типизации!
items.push(true);      // OK — потеря типизации!

// ПРАВИЛЬНО: Всегда указывайте тип
const numbers: number[] = [];  // number[]
numbers.push(1);               // OK
// numbers.push("два");        // Ошибка! Защита типизации работает

// Или используйте инициализацию
const strings = ["привет", "мир"]; // TypeScript выведет string[]
const scores = [10, 20, 30];       // TypeScript выведет number[]

Ошибка 5: Неправильное использование readonly

// ОШИБКА: Пытаемся изменить readonly массив
const readonlyArr: readonly number[] = [1, 2, 3];
// readonlyArr.push(4);     // Ошибка!
// readonlyArr[0] = 100;    // Ошибка!

// ПРАВИЛЬНО: Копируйте, если нужно изменить
const mutableArr = [...readonlyArr]; // Копия — можно изменять
mutableArr.push(4);                  // OK!

// Или используйте методы, возвращающие новый массив
const doubled = readonlyArr.map(n => n * 2);  // Новый массив
const filtered = readonlyArr.filter(n => n > 1); // Новый массив

// Если функция принимает readonly, можно передать обычный массив
function process(arr: readonly number[]): number {
  return arr.reduce((a, b) => a + b, 0);
}

const regularArray: number[] = [1, 2, 3];
console.log(process(regularArray)); // OK! Обычный массив совместим с readonly

Ошибка 6: Путаница с типами при деструктуризации

// ОШИБКА: Пытаемся деструктуризировать с неправильным порядком
const tuple: [string, number] = ["Анна", 25];

// Неправильно: порядок переменных не соответствует кортежу
// const [age, name] = tuple; // age будет строкой, name будет числом!

// Правильно: соблюдайте порядок
const [correctName, correctAge] = tuple;
console.log(correctName); // "Анна" (строка)
console.log(correctAge);  // 25 (число)

// Если порядок не важен — лучше использовать интерфейс
interface UserProfile {
  name: string;
  age: number;
}

const user: UserProfile = { name: "Анна", age: 25 };
const { name, age } = user; // Порядок не важен!

Ошибка 7: Использование any[] вместо конкретных типов

// ОШИБКА: Использование any[]
const items: any[] = [1, "два", true, { name: "объект" }];
// Потеряна вся типизация! Компилятор не поймает ошибки.

// ПРАВИЛЬНО: Используйте конкретные типы
const numbers: number[] = [1, 2, 3];
const strings: string[] = ["а", "б", "в"];

// Или объединение типов
const mixed: (number | string)[] = [1, "два", 3];

// Или интерфейс для сложных объектов
interface Item {
  id: number;
  name: string;
  active: boolean;
}

const items2: Item[] = [
  { id: 1, name: "Товар 1", active: true },
  { id: 2, name: "Товар 2", active: false }
];

Ошибка 8: Неправильное сравнение массивов

// ОШИБКА: Сравнение массивов через ===
const arr1 = [1, 2, 3];
const arr2 = [1, 2, 3];

console.log(arr1 === arr2); // false! Сравниваются ссылки, не содержимое

// ПРАВИЛЬНО: Сравнивайте поэлементно
function arraysEqual<T>(a: T[], b: T[]): boolean {
  if (a.length !== b.length) return false;
  return a.every((item, index) => item === b[index]);
}

console.log(arraysEqual(arr1, arr2)); // true

// Или используйте JSON.stringify для простых случаев
console.log(JSON.stringify(arr1) === JSON.stringify(arr2)); // true

Итоги урока

Что мы изучили:

  • Массивы — упорядоченные коллекции элементов одного типа. Обозначаются Type[] или Array<Type>
  • Смешанные массивы — используют объединение типов: (string | number)[]
  • Readonly массивы — нельзя изменить после создания: readonly Type[]
  • Кортежи — массивы с фиксированной длиной и типами: [Type1, Type2, ...]
  • Readonly кортежи — кортежи, которые нельзя изменить: readonly [Type1, Type2]
  • Деструктуризация — извлечение элементов в переменные: const [a, b] = tuple
  • Массивы в функциях — параметры, возвращаемые значения, rest-параметры

Совет для практики: Попробуйте создать небольшой проект — например, список задач (to-do list) или систему оценок студентов. Используйте массивы для хранения данных и кортежи для пар «ключ-значение» или результатов функций.

Ключевое правило: Используйте обычные массивы, когда длина и типы элементов могут меняться. Используйте кортежи, когда структура данных фиксирована и вы хотите, чтобы TypeScript знал тип каждого элемента по позиции.

Готовы к следующему уроку?

Следующий урок: Enum →

Проверяем понимание

8 вопросов

Кортеж из функции

Premium