Функциональное программирование в TypeScript: от теории к практике
Функциональное программирование (ФП) — это парадигма, в которой вычисления трактуются как вычисление математических функций и избегается изменение состояния и изменяемые данные. TypeScript, будучи надстройкой над JavaScript, отлично подходит для применения принципов ФП благодаря строгой типизации и поддержке функций высшего порядка. В этой статье мы разберем ключевые концепции ФП в TypeScript и покажем, как их применять на практике.
1. Чистые функции и иммутабельность
Чистая функция — это функция, которая:
- Всегда возвращает одинаковый результат для одних и тех же аргументов (детерминированность).
- Не имеет побочных эффектов (не изменяет внешние переменные, не выводит данные, не делает запросы).
Пример чистой функции:
// Чистая функция: суммирует два числа
const add = (a: number, b: number): number => a + b;
// Всегда вернёт 5
console.log(add(2, 3)); // 5
console.log(add(2, 3)); // 5
Иммутабельность — принцип, при котором данные не изменяются после создания. Вместо изменения существующего объекта создается новый.
// Иммутабельное обновление массива
const numbers: number[] = [1, 2, 3];
// Плохо (мутация):
numbers.push(4);
// Хорошо (иммутабельно):
const newNumbers = [...numbers, 4]; // [1,2,3,4]
// Иммутабельное обновление объекта
interface User {
name: string;
age: number;
}
const user: User = { name: 'Alice', age: 30 };
const updatedUser: User = { ...user, age: 31 }; // новый объект
2. Композиция функций и каррирование
Композиция функций — это объединение двух или более функций в одну новую. Результат первой функции передается как аргумент второй.
// Композиция двух функций
const compose = <T, U, V>(f: (x: U) => V, g: (x: T) => U) =>
(x: T): V => f(g(x));
const double = (x: number): number => x * 2;
const increment = (x: number): number => x + 1;
const doubleThenIncrement = compose(increment, double);
console.log(doubleThenIncrement(3)); // (3*2)+1 = 7
Каррирование — преобразование функции с несколькими аргументами в последовательность функций с одним аргументом.
// Обычная функция
const multiply = (a: number, b: number): number => a * b;
// Каррированная версия
const curriedMultiply = (a: number) => (b: number): number => a * b;
const multiplyBy2 = curriedMultiply(2);
console.log(multiplyBy2(5)); // 10
console.log(multiplyBy2(7)); // 14
Каррирование упрощает частичное применение и повторное использование функций.
3. Функторы и монады: Maybe и Either
Функтор — это контейнер, который реализует метод map, позволяющий применять функцию к значению внутри контейнера. В TypeScript функтором является, например, массив:
const numbers = [1, 2, 3];
const doubled = numbers.map(x => x * 2); // [2, 4, 6]
Монада — это функтор, который дополнительно реализует метод flatMap (или bind) и of (или return). Монады помогают обрабатывать побочные эффекты, null-значения и ошибки без исключений.
Реализуем простую монаду Maybe для безопасной работы с null/undefined:
// Монада Maybe
class Maybe<T> {
private constructor(private value: T | null) {}
static of<T>(value: T): Maybe<T> {
return new Maybe(value);
}
static none<T>(): Maybe<T> {
return new Maybe<T>(null);
}
map<U>(fn: (value: T) => U): Maybe<U> {
if (this.value === null || this.value === undefined) {
return Maybe.none();
}
return Maybe.of(fn(this.value));
}
flatMap<U>(fn: (value: T) => Maybe<U>): Maybe<U> {
if (this.value === null || this.value === undefined) {
return Maybe.none();
}
return fn(this.value);
}
getOrElse(defaultValue: T): T {
return this.value !== null ? this.value : defaultValue;
}
}
// Использование
interface UserData {
id: number;
address?: { city?: string };
}
const user: UserData = { id: 1, address: { city: 'Moscow' } };
const city = Maybe.of(user)
.map(u => u.address)
.flatMap(addr => Maybe.of(addr?.city))
.getOrElse('Unknown');
console.log(city); // Moscow
Аналогично реализуется монада Either для обработки ошибок без исключений:
// Простая реализация Either
type Either<L, R> = Left<L> | Right<R>;
class Left<L> {
constructor(readonly value: L) {}
isLeft(): this is Left<L> { return true; }
isRight(): this is Right<never> { return false; }
}
class Right<R> {
constructor(readonly value: R) {}
isLeft(): this is Left<never> { return false; }
isRight(): this is Right<R> { return true; }
}
// Функция, возвращающая Either
const divide = (a: number, b: number): Either<string, number> => {
if (b === 0) return new Left('Division by zero');
return new Right(a / b);
};
const result = divide(10, 2);
if (result.isRight()) {
console.log(`Result: ${result.value}`); // 5
} else {
console.error(`Error: ${result.value}`);
}
4. Практические паттерны: композиция через pipe и обработка потоков
В реальных проектах часто используется паттерн pipe — последовательное применение функций слева направо (в отличие от композиции).
// Реализация pipe
const pipe = <T>(...fns: Array<(arg: T) => T>) =>
(value: T): T => fns.reduce((acc,