remeda для TypeScript: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Библиотека remeda для TypeScript: Полное руководство

В мире современной веб-разработки на TypeScript обработка данных — одна из самых частых задач. Библиотека remeda (ранее известная как Remeda) представляет собой современную, type-safe альтернативу Lodash, специально разработанную для TypeScript. В этой статье мы подробно разберём, что такое remeda, как её установить, какие методы она предоставляет, и как использовать её в реальных проектах.

1. Что это и зачем нужна

Remeda — это библиотека утилит для функционального программирования на TypeScript. В отличие от Lodash, который изначально писался для JavaScript и лишь позднее получил типы, remeda с самого начала проектировалась с учётом строгой типизации. Это означает, что все функции в remeda полностью типобезопасны: компилятор TypeScript может вывести точные типы входных и выходных данных, что значительно снижает количество ошибок во время выполнения.

Зачем нужна remeda? Во-первых, она предоставляет удобные функции для работы с массивами, объектами, строками и другими структурами данных, которые часто используются в повседневной разработке. Во-вторых, remeda поощряет функциональный стиль программирования: большинство функций являются чистыми, не мутируют исходные данные и легко комбинируются с помощью пайпов (pipe). В-третьих, remeda имеет очень маленький размер (tree-shakeable), что позволяет включать в бандл только те функции, которые реально используются.

Основные преимущества remeda перед Lodash: полная поддержка TypeScript "из коробки", иммутабельность по умолчанию, поддержка пайпов (pipe) для композиции функций, а также более современный API, соответствующий стандартам ES6+.

2. Установка

Remeda распространяется через npm. Для установки выполните команду:

npm install remeda

Или, если вы используете yarn:

yarn add remeda

После установки вы можете импортировать функции как по отдельности (рекомендуемый способ для tree-shaking), так и весь модуль целиком:

// Импорт отдельных функций (рекомендуется)
import { pipe, map, filter } from 'remeda';

// Импорт всего модуля
import * as R from 'remeda';

3. Быстрый старт — минимальный рабочий пример

Давайте напишем простой пример: у нас есть массив чисел, мы хотим отфильтровать чётные числа, затем умножить каждое на 2 и вывести результат.

import { pipe, filter, map } from 'remeda';

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

const result = pipe(
    numbers,
    filter(x => x % 2 === 0), // [2, 4, 6]
    map(x => x * 2)           // [4, 8, 12]
);

console.log(result); // [4, 8, 12]

Как видите, код читается как последовательность операций: "взять numbers, отфильтровать чётные, затем умножить на 2". Это и есть суть функционального подхода, который продвигает remeda.

4. Основные методы/функции

Рассмотрим 8 ключевых функций remeda. Для каждой приведём сигнатуру, краткое описание и пример.

4.1. pipe

Сигнатура: pipe<A, B, C, ...>(value: A, ...fns: Function[]): LastReturnType

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

import { pipe } from 'remeda';

const addOne = (x: number) => x + 1;
const double = (x: number) => x * 2;

const result = pipe(5, addOne, double); // (5 + 1) * 2 = 12
console.log(result); // 12

4.2. map

Сигнатура: map<T, R>(fn: (value: T, index: number, array: T[]) => R): (array: T[]) => R[]

Что делает: Создаёт новый массив, применяя функцию-трансформер к каждому элементу исходного массива. Не мутирует исходный массив.

import { map } from 'remeda';

const numbers = [1, 2, 3];
const doubled = map(x => x * 2)(numbers);
console.log(doubled); // [2, 4, 6]

4.3. filter

Сигнатура: filter<T>(predicate: (value: T, index: number, array: T[]) => boolean): (array: T[]) => T[]

Что делает: Возвращает новый массив, содержащий только те элементы, для которых предикат вернул true.

import { filter } from 'remeda';

const numbers = [1, 2, 3, 4, 5];
const evens = filter(x => x % 2 === 0)(numbers);
console.log(evens); // [2, 4]

4.4. reduce

Сигнатура: reduce<T, R>(fn: (acc: R, value: T, index: number, array: T[]) => R, initial: R): (array: T[]) => R

Что делает: Свёртка массива в одно значение. Применяет функцию-аккумулятор к каждому элементу, передавая накопленное значение.

import { reduce } from 'remeda';

const numbers = [1, 2, 3, 4];
const sum = reduce((acc, x) => acc + x, 0)(numbers);
console.log(sum); // 10

4.5. find

Сигнатура: find<T>(predicate: (value: T, index: number, array: T[]) => boolean): (array: T[]) => T | undefined

Что делает: Возвращает первый элемент массива, удовлетворяющий предикату, или undefined, если такого элемента нет.

import { find } from 'remeda';

const users = [
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' },
    { id: 3, name: 'Charlie' }
];
const user = find(u => u.id === 2)(users);
console.log(user); // { id: 2, name: 'Bob' }

4.6. groupBy

Сигнатура: groupBy<T, K extends string | number>(fn: (value: T, index: number, array: T[]) => K): (array: T[]) => Record<K, T[]>

Что делает: Группирует элементы массива по ключу, возвращаемому функцией-селектором. Результат — объект, где ключи — значения селектора, а значения — массивы элементов.

import { groupBy } from 'remeda';

const people = [
    { name: 'Alice', age: 25 },
    { name: 'Bob', age: 30 },
    { name: 'Charlie', age: 25 }
];
const grouped = groupBy(p => p.age)(people);
console.log(grouped);
// {
//   '25': [ { name: 'Alice', age: 25 }, { name: 'Charlie', age: 25 } ],
//   '30': [ { name: 'Bob', age: 30 } ]
// }

4.7. sortBy

Сигнатура: sortBy<T>(...criteria: Array<(value: T) => number | string>): (array: T[]) => T[]

Что делает: Сортирует массив по одному или нескольким критериям. Возвращает новый отсортированный массив, не мутируя исходный.

import { sortBy } from 'remeda';

const products = [
    { name: 'Laptop', price: 1000 },
    { name: 'Mouse', price: 50 },
    { name: 'Keyboard', price: 150 }
];
const sorted = sortBy(p => p.price)(products);
console.log(sorted);
// [
//   { name: 'Mouse', price: 50 },
//   { name: 'Keyboard', price: 150 },
//   { name: 'Laptop', price: 1000 }
// ]

4.8. pick

Сигнатура: pick<T, K extends keyof T>(keys: K[]): (obj: T) => Pick<T, K>

Что делает: Создаёт новый объект, содержащий только указанные ключи из исходного объекта. Полезно для извлечения подмножества свойств.

import { pick } from 'remeda';

const user = {
    id: 1,
    name: 'Alice',
    email: 'alice@example.com',
    password: 'secret'
};
const publicData = pick(['id', 'name'])(user);
console.log(publicData); // { id: 1, name: 'Alice' }

5. Реальный практический пример

Представим, что мы разрабатываем систему управления заказами. У нас есть массив заказов, и нам нужно:

  • Отфильтровать только выполненные заказы (status === 'completed')
  • Сгруппировать их по категории товара
  • Для каждой категории посчитать общую выручку
  • Отсортировать категории по убыванию выручки
  • Выбрать топ-3 категории
import { pipe, filter, groupBy, map, reduce, sortBy, take, toPairs } from 'remeda';

// Исходные данные
interface Order {
    id: number;
    category: string;
    amount: number;
    status: 'pending' | 'completed' | 'cancelled';
}

const orders: Order[] = [
    { id: 1, category: 'electronics', amount: 1500, status: 'completed' },
    { id: 2, category: 'clothing', amount: 200, status: 'completed' },
    { id: 3, category: 'electronics', amount: 800, status: 'pending' },
    { id: 4, category: 'books', amount: 50, status: 'completed' },
    { id: 5, category: 'electronics', amount: 3000, status: 'completed' },
    { id: 6, category: 'clothing', amount: 100, status: 'completed' },
    { id: 7, category: 'books', amount: 30, status: 'cancelled' },
    { id: 8, category: 'home', amount: 500, status: 'completed' },
    { id: 9, category: 'electronics', amount: 1200, status: 'completed' },
    { id: 10, category: 'clothing', amount: 150, status: 'completed' }
];

// Решение
const topCategories = pipe(
    orders,
    // Шаг 1: Фильтруем только выполненные заказы
    filter(order => order.status === 'completed'),
    // Шаг 2: Группируем по категории
    groupBy(order => order.category),
    // Шаг 3: Преобразуем объект в массив пар [категория, массив заказов]
    toPairs(),
    // Шаг 4: Для каждой категории считаем сумму amount
    map(([category, categoryOrders]) => ({
        category,
        totalRevenue: pipe(
            categoryOrders,
            reduce((sum, order) => sum + order.amount, 0)
        )
    })),
    // Шаг 5: Сортируем по убыванию выручки
    sortBy(cat => -cat.totalRevenue),
    // Шаг 6: Берём топ-3
    take(3)
);

console.log('Топ-3 категории по выручке:');
topCategories.forEach((cat, index) => {
    console.log(`${index + 1}. ${cat.category}: ${cat.totalRevenue} руб.`);
});
// Вывод:
// 1. electronics: 5700 руб.
// 2. clothing: 450 руб.
// 3. home: 500 руб. или books: 50 руб. (зависит от данных)

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

6. Советы по производительности и типичные ошибки

Советы по производительности:

  • Используйте tree-shaking: Импортируйте только нужные функции, а не весь модуль. Это уменьшит размер бандла.
  • Избегайте избыточных проходов: В отличие от Lodash, remeda не оптимизирует цепочки вызовов автоматически. Если вам нужно сделать filter и map, это будет два прохода по массиву. Для больших массивов (более 10 000 элементов) рассмотрите использование одного reduce.
  • Используйте pipe для композиции: Pipe создаёт замыкание, но не добавляет накладных расходов на производительность по сравнению с ручной композицией.
  • Будьте осторожны с глубокими цепочками: Каждый вызов pipe создаёт новый массив. Для очень длинных цепочек (более 10 шагов) может быть выгоднее использовать транзакционные подходы.

Типичные ошибки:

  • Забыли импортировать функцию: Remeda не экспортирует все функции через дефолтный экспорт. Всегда проверяйте, что вы импортировали нужную функцию.
  • Мутация данных внутри pipe: Remeda функции не мутируют данные, но если вы передаёте мутирующую функцию (например, push в массиве), это может привести к не

Рекомендаций