Библиотека 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 в массиве), это может привести к не
Также в библиотеке
Авторизация и аутентификация в TypeScript: полное руководство
Jetpack Compose для Kotlin: полное руководство с примерами
Prophecy для PHP: полное руководство с примерами
Реактивное программирование в Swift: Combine и RxSwift для iOS/macOS