ts-functional-pipe для TypeScript: полное руководство с примерами
1. Что это и зачем нужна
ts-functional-pipe — это легковесная библиотека для TypeScript, которая предоставляет мощный и типобезопасный оператор пайпа (pipe) и набор утилит для функциональной композиции. В отличие от встроенного предложения по оператору |> в JavaScript (который пока не стандартизирован), эта библиотека позволяет уже сегодня использовать элегантный и читаемый синтаксис для последовательной обработки данных. Основная идея заключается в том, чтобы заменить вложенные вызовы функций (f(g(h(x)))) на линейную цепочку (pipe(x, h, g, f)), что значительно улучшает читаемость и поддерживаемость кода.
Зачем это нужно? В современной разработке на TypeScript мы часто сталкиваемся с необходимостью выполнять несколько последовательных преобразований над данными: фильтрация, маппинг, сортировка, валидация, трансформация. Без пайпа такой код превращается в "пирамиду" из вложенных функций или в цепочку мутабельных переменных. ts-functional-pipe решает эту проблему, предоставляя декларативный способ описания потока данных. Библиотека полностью типизирована, что означает, что TypeScript будет проверять типы на каждом этапе пайпа, предотвращая ошибки на этапе компиляции.
Библиотека особенно полезна в проектах, где используется функциональный подход: обработка массивов, работа с Observable (RxJS), трансформация ответов от API, построение middleware и многое другое. Она не навязывает никаких парадигм, а просто дает удобный инструмент для композиции функций. ts-functional-pipe — это не просто pipe, это целый набор функций-помощников, которые делают функциональное программирование в TypeScript комфортным и безопасным.
2. Установка
Установка библиотеки производится стандартным менеджером пакетов npm (или yarn/pnpm). Библиотека написана на TypeScript и поставляется с готовыми файлами деклараций типов, поэтому дополнительная установка @types не требуется.
# Установка через npm
npm install ts-functional-pipe
# Установка через yarn
yarn add ts-functional-pipe
# Установка через pnpm
pnpm add ts-functional-pipe
После установки вы можете импортировать необходимые функции в ваш проект:
import { pipe, compose, map, filter, reduce } from 'ts-functional-pipe';
3. Быстрый старт — минимальный рабочий пример
Давайте начнем с самого простого примера: преобразуем число, применив к нему несколько операций. Без библиотеки это выглядело бы как Math.sqrt(Math.abs(-25)). С ts-functional-pipe мы можем записать это линейно:
import { pipe } from 'ts-functional-pipe';
// Определяем простые функции
const addOne = (x: number): number => x + 1;
const double = (x: number): number => x * 2;
const toString = (x: number): string => `Результат: ${x}`;
// Используем pipe для последовательного применения
const result = pipe(
5, // Начальное значение
addOne, // 5 + 1 = 6
double, // 6 * 2 = 12
toString // "Результат: 12"
);
console.log(result); // Вывод: "Результат: 12"
Как видите, код читается сверху вниз, что намного интуитивнее, чем вложенные вызовы. pipe автоматически передает результат предыдущей функции как аргумент следующей.
4. Основные методы/функции
Библиотека предоставляет богатый набор функций. Рассмотрим 8 ключевых из них.
4.1. pipe
Сигнатура: pipe<A, B, C, ..., Z>(value: A, ...fns: [(a: A) => B, (b: B) => C, ..., (y: Y) => Z]): Z
Что делает: Принимает начальное значение и список функций. Последовательно применяет каждую функцию к результату предыдущей. Первая функция получает начальное значение.
import { pipe } from 'ts-functional-pipe';
const toUpperCase = (s: string) => s.toUpperCase();
const exclaim = (s: string) => s + '!';
const repeat = (s: string) => s + ' ' + s;
const result = pipe('hello', toUpperCase, exclaim, repeat);
console.log(result); // "HELLO! HELLO!"
4.2. compose
Сигнатура: compose<A, B, C, ..., Z>(...fns: [...functions]): (value: A) => Z
Что делает: Композирует функции справа налево. Возвращает новую функцию, которая применяет функции в обратном порядке. Отличается от pipe порядком применения.
import { compose } from 'ts-functional-pipe';
const toUpperCase = (s: string) => s.toUpperCase();
const exclaim = (s: string) => s + '!';
// compose применяет функции справа налево: сначала exclaim, потом toUpperCase
const shout = compose(toUpperCase, exclaim);
console.log(shout('hello')); // "HELLO!" (сначала 'hello!', потом 'HELLO!')
4.3. map
Сигнатура: map<T, R>(fn: (item: T, index: number) => R): (arr: T[]) => R[]
Что делает: Создает пайпабельную версию Array.prototype.map. Принимает функцию-трансформер и возвращает новую функцию, которая применяет этот трансформер к каждому элементу массива.
import { pipe, map } from 'ts-functional-pipe';
const numbers = [1, 2, 3, 4];
const result = pipe(
numbers,
map((x: number) => x * 10),
map((x: number) => `$${x}`)
);
console.log(result); // ["$10", "$20", "$30", "$40"]
4.4. filter
Сигнатура: filter<T>(predicate: (item: T, index: number) => boolean): (arr: T[]) => T[]
Что делает: Пайпабельная версия Array.prototype.filter. Фильтрует массив, оставляя только те элементы, для которых предикат возвращает true.
import { pipe, filter } from 'ts-functional-pipe';
const numbers = [1, 2, 3, 4, 5, 6];
const isEven = (x: number) => x % 2 === 0;
const result = pipe(numbers, filter(isEven));
console.log(result); // [2, 4, 6]
4.5. reduce
Сигнатура: reduce<T, R>(reducer: (acc: R, item: T, index: number) => R, initial: R): (arr: T[]) => R
Что делает: Пайпабельная версия Array.prototype.reduce. Сворачивает массив в одно значение, применяя функцию-редьюсер к каждому элементу и аккумулятору.
import { pipe, reduce } from 'ts-functional-pipe';
const numbers = [1, 2, 3, 4, 5];
const sum = pipe(
numbers,
reduce((acc: number, x: number) => acc + x, 0)
);
console.log(sum); // 15
4.6. tap
Сигнатура: tap<T>(fn: (value: T) => void): (value: T) => T
Что делает: Функция "прослушки". Выполняет побочный эффект (например, логирование) для текущего значения, но возвращает само значение без изменений. Полезна для отладки.
import { pipe, tap, map } from 'ts-functional-pipe';
const numbers = [1, 2, 3];
const result = pipe(
numbers,
tap((arr) => console.log('Before map:', arr)),
map((x: number) => x * 2),
tap((arr) => console.log('After map:', arr))
);
// Вывод в консоль:
// Before map: [1, 2, 3]
// After map: [2, 4, 6]
console.log(result); // [2, 4, 6]
4.7. prop
Сигнатура: prop<T, K extends keyof T>(key: K): (obj: T) => T[K]
Что делает: Возвращает новую функцию, которая извлекает указанное свойство из объекта. Это удобно для работы с массивами объектов.
import { pipe, map, prop } from 'ts-functional-pipe';
interface User {
id: number;
name: string;
age: number;
}
const users: User[] = [
{ id: 1, name: 'Alice', age: 30 },
{ id: 2, name: 'Bob', age: 25 },
];
const names = pipe(
users,
map(prop('name'))
);
console.log(names); // ["Alice", "Bob"]
4.8. curry
Сигнатура: curry<TArgs extends any[], R>(fn: (...args: TArgs) => R): CurriedFunction<TArgs, R>
Что делает: Каррирует функцию. Позволяет вызывать функцию с частичным набором аргументов, возвращая новую функцию, ожидающую оставшиеся аргументы.
import { curry } from 'ts-functional-pipe';
const add = (a: number, b: number, c: number) => a + b + c;
const curriedAdd = curry(add);
const add5 = curriedAdd(5); // частичное применение
const add5And10 = add5(10);
const result = add5And10(20); // 5 + 10 + 20 = 35
console.log(result); // 35
// Можно вызвать сразу все аргументы
console.log(curriedAdd(1, 2, 3)); // 6
5. Реальный практический пример
Рассмотрим реальный сценарий: обработка данных о заказах из интернет-магазина. У нас есть массив заказов, и нам нужно:
- Отфильтровать только выполненные заказы (status === 'completed').
- Выбрать только заказы дороже 1000 рублей.
- Извлечь названия товаров.
- Привести их к нижнему регистру.
- Убрать дубликаты.
- Отсортировать по алфавиту.
- Собрать в одну строку через запятую.
Без ts-functional-pipe это был бы набор вложенных функций или мутабельных переменных. С библиотекой код становится декларативным и легко читаемым.
import {
pipe,
filter,
map,
prop,
reduce,
sort,
unique,
toLower
} from 'ts-functional-pipe';
// Типы данных
interface Order {
id: number;
status: 'pending' | 'completed' | 'cancelled';
total: number;
items: { name: string; price: number }[];
}
// Входные данные
const orders: Order[] = [
{ id: 1, status: 'completed', total: 2500, items: [{ name: 'Ноутбук', price: 2500 }] },
{ id: 2, status: 'completed', total: 500, items: [{ name: 'Мышь', price: 500 }] },
{ id: 3, status: 'pending', total: 1500, items: [{ name: 'Монитор', price: 1500 }] },
{ id: 4, status: 'completed', total: 3000, items: [{ name: 'Клавиатура', price: 1000 }, { name: 'Коврик', price: 2000 }] },
{ id: 5, status: 'completed', total: 1200, items: [{ name: 'Наушники', price: 1200 }] },
{ id: 6, status: 'completed', total: 800, items: [{ name: 'Флешка', price: 800 }] },
];
// Вспомогательные функции
const isCompleted = (order: Order) => order.status === 'completed';
const isExpensive = (order: Order) => order.total > 1000;
const getItemNames = (order: Order) => order.items.map(prop('name'));
const flatten = (arr: string[][]) => arr.reduce((acc, val) => acc.concat(val), []);
const toLowerCase = (s: string) => s.toLowerCase();
// Основной пайплайн
const result = pipe(
orders,
filter(isCompleted), // Шаг 1: только выполненные
filter(isExpensive), // Шаг 2: дороже 1000
map(getItemNames), // Шаг 3: извлекаем названия товаров
flatten, // Шаг 4: разворачиваем массив массивов
map(toLowerCase), // Шаг 5: в нижний регистр
unique, // Шаг 6: убираем дубликаты
sort(), // Ш Также в библиотеке
WebSocket библиотеки для Node.js: ws, Socket.IO, uWS — полный обзор
FluentValidation для C# (.NET): полное руководство с примерами
Отправка SMS на TypeScript: API библиотеки sms-client-ts
Логирование для Swift: библиотека SwiftyBeaver
Тестирование в Kotlin: фреймворки и инструменты для качественного кода