📘 TypeScript

Array and Collection Methods in TypeScript: A Detailed Guide

Что это такое и зачем? (аналогия из реальной жизни)

Представь, что ты — библиотекарь. У тебя есть огромный стеллаж с книгами (это наш массив). Каждая книга стоит на своей полке, и у каждой есть номер (индекс).

Что ты можешь делать с книгами?

  • Взять книгу с полки по номеру — это array[0]
  • Добавить новую книгу в конец стеллажа — это push()
  • Убрать последнюю книгу — это pop()
  • Найти книгу по названию — это find()
  • Проверить, есть ли книга автора "Пушкин" — это some()
  • Отфильтровать только книги в твёрдой обложке — это filter()
  • Превратить список книг в список авторов — это map()

Методы массивов — это готовые инструменты, которые JavaScript (и TypeScript) даёт нам, чтобы не писать каждый раз циклы вручную. Вместо того чтобы перебирать книги пальцем (цикл for), мы просто говорим: "Найди мне книгу по условию" — и метод делает всё сам.

💡 Важно: Методы делятся на две группы: мутирующие (изменяют исходный массив) и не мутирующие (возвращают новый массив, оставляя старый без изменений). В современном программировании стараются использовать не мутирующие методы — это безопаснее.

Теория: как это работает изнутри

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

Когда мы вызываем метод, например array.push(5), происходит следующее:

  1. JavaScript проверяет, есть ли свободное место в текущем блоке памяти
  2. Если места нет — выделяется новый, больший блок памяти (обычно в 1.5-2 раза больше)
  3. Все старые элементы копируются в новый блок
  4. Новый элемент добавляется в конец
  5. Старый блок памяти помечается для удаления (сборщик мусора)

Методы map, filter, reduce работают по принципу функционального программирования: они принимают функцию-колбэк и применяют её к каждому элементу массива.

🔬 Сложность алгоритмов (Big O):
  • push/pop — O(1) (мгновенно)
  • shift/unshift — O(n) (медленно, так как нужно сдвигать все элементы)
  • find/indexOf — O(n) (нужно проверить каждый элемент)
  • sort — O(n log n) (быстрая сортировка)

Синтаксис в TypeScript: все формы и варианты

TypeScript добавляет к методам JavaScript типизацию. Это значит, что мы указываем, какие типы данных хранятся в массиве, и TypeScript проверяет, что мы не положим туда что-то лишнее.

Объявление массивов

// Способ 1: квадратные скобки (рекомендуется)
let numbers: number[] = [1, 2, 3, 4, 5];

// Способ 2: generic Array
let names: Array<string> = ["Анна", "Борис", "Виктор"];

// Способ 3: readonly (неизменяемый массив)
let fixed: readonly number[] = [10, 20, 30];
// fixed.push(40); // ❌ Ошибка! Нельзя изменить readonly массив

// Способ 4: кортеж (tuple) — массив фиксированной длины с разными типами
let person: [string, number, boolean] = ["Иван", 25, true];

Таблица основных методов массивов

Метод Что делает Мутирует? Возвращает
push(element) Добавляет элемент в конец ✅ Да Новую длину массива
pop() Удаляет последний элемент ✅ Да Удалённый элемент
shift() Удаляет первый элемент ✅ Да Удалённый элемент
unshift(element) Добавляет элемент в начало ✅ Да Новую длину массива
map(callback) Преобразует каждый элемент ❌ Нет Новый массив
filter(callback) Оставляет элементы по условию ❌ Нет Новый массив
reduce(callback, init) Сворачивает массив в одно значение ❌ Нет Одно значение
find(callback) Находит первый подходящий элемент ❌ Нет Элемент или undefined
some(callback) Проверяет, есть ли хотя бы один подходящий ❌ Нет boolean
every(callback) Проверяет, все ли подходят ❌ Нет boolean
includes(element) Проверяет наличие элемента ❌ Нет boolean
slice(start, end) Вырезает часть массива ❌ Нет Новый массив
splice(start, count, ...items) Удаляет/добавляет элементы ✅ Да Массив удалённых элементов
sort(compareFn) Сортирует массив ✅ Да Тот же массив (ссылка)
reverse() Переворачивает массив ✅ Да Тот же массив (ссылка)
concat(...arrays) Объединяет массивы ❌ Нет Новый массив
flat(depth) Разворачивает вложенные массивы ❌ Нет Новый массив
flatMap(callback) map + flat глубины 1 ❌ Нет Новый массив

Типизация колбэков

// TypeScript автоматически выводит типы параметров колбэка
const numbers: number[] = [1, 2, 3, 4, 5];

// Параметры: (элемент, индекс, весьМассив)
const doubled = numbers.map((num: number, index: number, arr: number[]) => {
    console.log(`Элемент ${num} на позиции ${index} в массиве [${arr}]`);
    return num * 2;
});

// Можно не указывать типы — TypeScript выведет их сам
const filtered = numbers.filter(item => item > 2);

Примеры кода — от простого к сложному

Пример 1: Базовые операции (push, pop, shift, unshift)

// Создаём массив чисел
let queue: number[] = [1, 2, 3];

// Добавляем в конец (как встать в очередь)
queue.push(4);  // queue = [1, 2, 3, 4]
console.log("После push:", queue);

// Убираем с конца (как уйти из очереди)
let last = queue.pop();  // last = 4, queue = [1, 2, 3]
console.log("Удалили:", last, "Осталось:", queue);

// Добавляем в начало (как пролезть без очереди)
queue.unshift(0);  // queue = [0, 1, 2, 3]
console.log("После unshift:", queue);

// Убираем из начала (как уйти первым)
let first = queue.shift();  // first = 0, queue = [1, 2, 3]
console.log("Удалили:", first, "Осталось:", queue);

Пример 2: map — преобразование данных

// У нас есть массив температур в Цельсиях
const celsius: number[] = [0, 10, 20, 30, 40];

// Преобразуем в Фаренгейты: F = C * 9/5 + 32
const fahrenheit: number[] = celsius.map(temp => {
    return temp * 9/5 + 32;
});

console.log("Цельсии:", celsius);      // [0, 10, 20, 30, 40]
console.log("Фаренгейты:", fahrenheit); // [32, 50, 68, 86, 104]

// Важно: исходный массив не изменился!

Пример 3: filter — отбор по условию

// Массив студентов с баллами
interface Student {
    name: string;
    score: number;
}

const students: Student[] = [
    { name: "Анна", score: 85 },
    { name: "Борис", score: 42 },
    { name: "Виктор", score: 73 },
    { name: "Галина", score: 91 },
    { name: "Дмитрий", score: 38 }
];

// Отбираем тех, кто сдал (балл >= 60)
const passed: Student[] = students.filter(student => student.score >= 60);

console.log("Сдали экзамен:");
passed.forEach(s => console.log(`${s.name}: ${s.score} баллов`));
// Вывод: Анна: 85, Виктор: 73, Галина: 91

// Можно комбинировать: отбираем и преобразуем
const topStudents: string[] = students
    .filter(s => s.score >= 80)      // сначала фильтруем
    .map(s => s.name);               // потом берём только имена

console.log("Лучшие студенты:", topStudents); // ["Анна", "Галина"]

Пример 4: reduce — вычисление итога

// Суммируем числа
const numbers: number[] = [10, 20, 30, 40, 50];

// reduce принимает два параметра:
// 1. Колбэк (аккумулятор, текущий элемент) => новое значение
// 2. Начальное значение аккумулятора
const sum: number = numbers.reduce((acc, current) => {
    return acc + current;
}, 0);  // начинаем с 0

console.log("Сумма:", sum); // 150

// Более сложный пример: группировка по свойству
const fruits: string[] = ["яблоко", "банан", "яблоко", "апельсин", "банан", "яблоко"];

const fruitCount: Record<string, number> = fruits.reduce((acc, fruit) => {
    // Если фрукт уже есть — увеличиваем счётчик, иначе ставим 1
    acc[fruit] = (acc[fruit] || 0) + 1;
    return acc;
}, {} as Record<string, number>);

console.log("Подсчёт фруктов:", fruitCount);
// { "яблоко": 3, "банан": 2, "апельсин": 1 }

Пример 5: find, some, every — поиск и проверка

interface Product {
    id: number;
    name: string;
    price: number;
    inStock: boolean;
}

const products: Product[] = [
    { id: 1, name: "Ноутбук", price: 75000, inStock: true },
    { id: 2, name: "Мышь", price: 1500, inStock: false },
    { id: 3, name: "Клавиатура", price: 3000, inStock: true },
    { id: 4, name: "Монитор", price: 25000, inStock: true }
];

// find — находит ПЕРВЫЙ подходящий элемент
const firstAvailable: Product | undefined = products.find(p => p.inStock);
console.log("Первый в наличии:", firstAvailable?.name); // "Ноутбук"

// findIndex — находит индекс первого подходящего
const mouseIndex: number = products.findIndex(p => p.name === "Мышь");
console.log("Индекс мыши:", mouseIndex); // 1

// some — есть ли хоть один товар дороже 50000?
const hasExpensive: boolean = products.some(p => p.price > 50000);
console.log("Есть дорогие товары?", hasExpensive); // true

// every — все ли товары в наличии?
const allInStock: boolean = products.every(p => p.inStock);
console.log("Все в наличии?", allInStock); // false (мышь не в наличии)

// includes — проверка наличия примитива
const ids: number[] = [1, 2, 3, 4, 5];
console.log("Есть id 3?", ids.includes(3)); // true
console.log("Есть id 10?", ids.includes(10)); // false

Пример 6: slice, splice — копирование и удаление

const letters: string[] = ["а", "б", "в", "г", "д", "е"];

// slice — НЕ мутирует, возвращает новый массив
// slice(start, end) — end не включается
const firstThree: string[] = letters.slice(0, 3);
console.log("Первые три:", firstThree); // ["а", "б", "в"]
console.log("Оригинал не изменился:", letters); // ["а", "б", "в", "г", "д", "е"]

// slice без аргументов — полная копия
const copy: string[] = letters.slice();

// splice — мутирует, удаляет/добавляет элементы
// splice(старт, сколько_удалить, ...что_добавить)
const removed: string[] = letters.splice(2, 2, "х", "у");
console.log("Удалили:", removed); // ["в", "г"]
console.log("Массив после splice:", letters); // ["а", "б", "х", "у", "д", "е"]

// toSpliced — НЕ мутирует (ES2023+)
const safeRemoved: string[] = letters.toSpliced(1, 1);
console.log("Безопасная версия:", safeRemoved);
console.log("Оригинал цел:", letters);

Пример 7: sort — сортировка с компаратором

// Внимание: sort() по умолчанию сортирует как строки!
const numbers: number[] = [10, 5, 20, 1, 30];
numbers.sort();
console.log("Сортировка по умолчанию:", numbers); // [1, 10, 20, 30, 5] — неправильно!

// Правильная сортировка чисел: передаём функцию сравнения
numbers.sort((a, b) => a - b);  // по возрастанию
console.log("По возрастанию:", numbers); // [1, 5, 10, 20, 30]

numbers.sort((a, b) => b - a);  // по убыванию
console.log("По убыванию:", numbers); // [30, 20, 10, 5, 1]

// Сортировка объектов по свойству
interface Person {
    name: string;
    age: number;
}

const people: Person[] = [
    { name: "Иван", age: 30 },
    { name: "Анна", age: 25 },
    { name: "Борис", age: 35 }
];

// Сортируем по возрасту
people.sort((a, b) => a.age - b.age);
console.log("По возрасту:", people);
// [{ name: "Анна", age: 25 }, { name: "Иван", age: 30 }, { name: "Борис", age: 35 }]

// Сортируем по имени (строки)
people.sort((a, b) => a.name.localeCompare(b.name));
console.log("По имени:", people);
// [{ name: "Анна", age: 25 }, { name: "Борис", age: 35 }, { name: "Иван", age: 30 }]

// toSorted — НЕ мутирует (ES2023+)
const sortedPeople = people.toSorted((a, b) => a.age - b.age);

Пример 8: flat и flatMap — работа с вложенными массивами

// flat — разворачивает вложенные массивы
const nested: number[][] = [[1, 2], [3, 4], [5, 6]];
const flat: number[] = nested.flat();
console.log("Развёрнутый:", flat); // [1, 2, 3, 4, 5, 6]

// flat с глубиной
const deepNested: any[] = [1, [2, [3, [4]]]];
const depth1 = deepNested.flat(1);  // [1, 2, [3, [4]]]
const depth2 = deepNested.flat(2);  // [1, 2, 3, [4]]
const depth3 = deepNested.flat(Infinity); // [1, 2, 3, 4]

// flatMap — map + flat глубины 1
const sentences: string[] = ["Привет мир", "Как дела", "Всё хорошо"];
const words: string[] = sentences.flatMap(sentence => sentence.split(" "));
console.log("Слова:", words);
// ["Привет", "мир", "Как", "дела", "Всё", "хорошо"]

// flatMap удобен, когда нужно вернуть несколько элементов из одного
const numbers2: number[] = [1, 2, 3];
const expanded = numbers2.flatMap(n => [n, n * 10]);
console.log("Расширенный:", expanded); // [1, 10, 2, 20, 3, 30]

Пример 9: Цепочки методов (method chaining)

interface Order {
    id: number;
    amount: number;
    status: "pending" | "completed" | "cancelled";
}

const orders: Order[] = [
    { id: 1, amount: 5000, status: "completed" },
    { id: 2, amount: 3000, status: "pending" },
    { id: 3, amount: 8000, status: "completed" },
    { id: 4, amount: 1000, status: "cancelled" },
    { id: 5, amount: 6000, status: "completed" }
];

// Цепочка: фильтруем → преобразуем → сортируем → берём первые 2
const topCompleted: number[] = orders
    .filter(order => order.status === "completed")  // оставляем только выполненные
    .map(order => order.amount)                     // берём только суммы
    .sort((a, b) => b - a)                          // сортируем по убыванию
    .slice(0, 2);                                   // берём первые 2

console.log("Две самые большие суммы среди выполненных:", topCompleted);
// [8000, 6000]

// Можно разбить для читаемости:
const completedOrders = orders.filter(o => o.status === "completed");
const amounts = completedOrders.map(o => o.amount);
const sortedAmounts = amounts.sort((a, b) => b - a);
const topTwo = sortedAmounts.slice(0, 2);

Сравнение с Python: чем похоже и чем отличается

Операция TypeScript Python
Добавить в конец arr.push(x) arr.append(x)
Удалить с конца arr.pop() arr.pop()
Добавить в начало arr.unshift(x) arr.insert(0, x)
Преобразовать arr.map(fn) map(fn, arr) или [fn(x) for x in arr]
Отфильтровать arr.filter(fn) filter(fn, arr) или [x for x in arr if cond]
Свернуть arr.reduce(fn, init) functools.reduce(fn, arr, init)
Проверить наличие arr.includes(x) x in arr
Длина arr.length len(arr)
Срез arr.slice(1, 3) arr[1:3]
Сортировка arr.sort(fn) arr.sort(key=fn)
Развернуть arr.reverse() arr.reverse()
Объединить arr1.concat(arr2) arr1 + arr2
Все элементы подходят? arr.every(fn) all(fn(x) for x in arr)
Хотя бы один подходит? arr.some(fn) any(fn(x) for x in arr)

Ключевые отличия:

  • Типизация: TypeScript проверяет типы на этапе компиляции, Python — динамическая типизация
  • Мутабельность: В Python почти все методы списков мутирующие. В TypeScript/JS есть и мутирующие, и нет
  • Синтаксис: Python использует синтаксис с отступами и list comprehensions, TypeScript — фигурные скобки и методы
  • Производительность: В JS методы массивов оптимизированы движками (V8), в Python — нативные C-функции

Типичные ошибки и подводные камни

Ошибка 1: Забыть, что sort() сортирует как строки

const nums = [10, 5, 20, 1];
nums.sort(); // ❌ [1, 10, 20, 5] — строковая сортировка!
// ✅ Правильно:
nums.sort((a, b) => a - b);

Ошибка 2: Мутация исходного массива

const original = [1, 2, 3];
const copy = original;
copy.push(4); // ❌ Мутирует и original тоже!
console.log(original); // [1, 2, 3, 4]

// ✅ Правильно: создавать копию
const safeCopy = [...original, 4];
// или
const safeCopy2 = original.concat(4);

Ошибка 3: Не обрабатывать undefined от find

const items = [{ id: 1, name: "A" }, { id: 2, name: "B" }];
const found = items.find(item => item.id === 3);
console.log(found.name); // ❌ Ошибка! found — undefined

// ✅ Правильно: проверять
if (found) {
    console.log(found.name);
}
// или использовать optional chaining
console.log(found?.name); // undefined, без ошибки

Ошибка 4: Путать slice и splice

const arr = [1, 2, 3, 4, 5];
const sliced = arr.slice(1, 3);  // [2, 3] — не мутирует
const spliced = arr.splice(1, 3); // [2, 3, 4] — мутирует arr!

// Запомните: slice — копирует (как ножницы), splice — вырезает (как хирург)

Ошибка 5: Забыть про shallow copy

const users = [{ name: "Иван" }, { name: "Мария" }];
const copy = [...users]; // поверхностная копия
copy[0].name = "Пётр"; // ❌ Изменяет и original[0].name!
console.log(users[0].name); // "Пётр"

// ✅ Для глубокого копирования:
const deepCopy = JSON.parse(JSON.stringify(users));
// или structuredClone (современный браузер)
const deepCopy2 = structuredClone(users);

Ошибка 6: Использовать forEach вместо map

const nums = [1, 2, 3];
const doubled = [];
nums.forEach(n => doubled.push(n * 2)); // ❌ Неправильный подход

// ✅ Правильно:
const doubledCorrect = nums.map(n => n * 2);

Лучшие практики и советы профессионалов

  1. Отдавайте предпочтение неизменяемости

    Используйте методы, которые не мутируют исходный массив: map, filter, slice, toSorted, toSpliced, toReversed (ES2023+). Это делает код предсказуемым и легче отлаживаемым.

  2. Используйте readonly для защиты
    function processItems(items: readonly number[]): number {
        // items.push(5); // ❌ Ошибка компиляции
        return items.reduce((sum, n) => sum + n, 0);
    }
  3. Избегайте мутации в цепочках методов
    // ❌ Плохо: sort мутирует
    const result = arr.filter(fn).sort().map(fn2);
    
    // ✅ Хорошо: используем toSorted
    const result = arr.filter(fn).toSorted().map(fn2);
    
  4. Используйте деструктуризацию для ясности
    // Вместо
    users.map(function(user) { return user.name; });
    // Лучше
    users.map(({ name }) => name);
    
  5. Пишите типы для сложных операций
    // Явно указываем тип результата reduce
    const total: number = items.reduce((acc, item) => acc + item.price, 0);
    
    // Для группировки используем Record
    const grouped: Record<string, Item[]> = items.reduce(/* ... */, {} as Record<string, Item[]>);
    
  6. Используйте flatMap вместо map + filter + flat
    // ❌ Сложно
    const result = arr