Функциональное программирование в TypeScript: продвинутый уровень
Функциональное программирование (ФП) — это парадигма, которая рассматривает вычисления как оценку математических функций и избегает изменения состояния и изменяемых данных. TypeScript, являясь надстройкой над JavaScript, предоставляет мощную систему типов, которая позволяет реализовать многие концепции ФП на новом уровне безопасности и выразительности. В этой статье мы рассмотрим продвинутые техники: от строгой иммутабельности до работы с монадами и функторами.
Иммутабельность и работа с неизменяемыми структурами
Иммутабельность (неизменяемость) — краеугольный камень ФП. В TypeScript мы можем гарантировать её на уровне типов с помощью модификаторов readonly и утилитарных типов.
// Глубокая неизменяемость через ReadonlyDeep
type DeepReadonly<T> = {
readonly [P in keyof T]: DeepReadonly<T[P]>;
};
interface User {
name: string;
address: { city: string; zip: number };
}
const user: DeepReadonly<User> = {
name: "Alice",
address: { city: "Moscow", zip: 101000 }
};
// user.address.city = "SPb"; // Ошибка! Свойство только для чтения
Для безопасного обновления данных используйте чистые функции, возвращающие новые объекты:
function updateZip(user: DeepReadonly<User>, newZip: number): DeepReadonly<User> {
return { ...user, address: { ...user.address, zip: newZip } };
}
const updatedUser = updateZip(user, 190000);
Композиция функций и каррирование
Композиция позволяет строить сложные преобразования из простых функций. В TypeScript с помощью дженериков мы можем создавать типобезопасные комбинаторы.
// Тип для композиции двух функций
type Compose<A, B, C> = (f: (b: B) => C, g: (a: A) => B) => (a: A) => C;
const compose: Compose<any, any, any> = (f, g) => (x) => f(g(x));
// Пример: преобразование строки в число и увеличение на 1
const toNumber = (s: string): number => parseInt(s, 10);
const increment = (n: number): number => n + 1;
const parseAndIncrement = compose(increment, toNumber);
console.log(parseAndIncrement("5")); // 6
Каррирование превращает функцию от нескольких аргументов в последовательность функций от одного аргумента. Это делает композицию более гибкой:
// Универсальная функция каррирования с типами
function curry<T1, T2, R>(fn: (a: T1, b: T2) => R): (a: T1) => (b: T2) => R {
return (a: T1) => (b: T2) => fn(a, b);
}
const add = (a: number, b: number): number => a + b;
const curriedAdd = curry(add);
const add5 = curriedAdd(5);
console.log(add5(3)); // 8
Функторы и монады: работа с контекстами
Функтор — это структура данных, которая поддерживает операцию map. В TypeScript функтором может быть массив, Promise или пользовательский тип.
// Функтор Option (Maybe) для безопасной работы с null/undefined
interface Option<T> {
map<U>(fn: (value: T) => U): Option<U>;
getOrElse(defaultValue: T): T;
}
class Some<T> implements Option<T> {
constructor(private value: T) {}
map<U>(fn: (value: T) => U): Option<U> {
return new Some(fn(this.value));
}
getOrElse(_: T): T {
return this.value;
}
}
class None<T> implements Option<T> {
map<U>(_: (value: T) => U): Option<U> {
return new None<U>();
}
getOrElse(defaultValue: T): T {
return defaultValue;
}
}
// Использование
const maybeNumber: Option<number> = new Some(42);
const result = maybeNumber.map(x => x * 2).getOrElse(0); // 84
Монада расширяет функтор методом flatMap (или bind), позволяя избегать вложенности контекстов. Реализуем монаду для работы с асинхронными операциями:
// Простая монада Task (аналог Promise, но ленивый)
class Task<T> {
constructor(private run: () => Promise<T>) {}
map<U>(fn: (value: T) => U): Task<U> {
return new Task(() => this.run().then(fn));
}
flatMap<U>(fn: (value: T) => Task<U>): Task<U> {
return new Task(() => this.run().then(value => fn(value).run()));
}
execute(): Promise<T> {
return this.run();
}
}
// Пример: цепочка асинхронных операций
const getUser = (id: number): Task<{ name: string; age: number }> =>
new Task(() => Promise.resolve({ name: "Bob", age: 30 }));
const getAgeCategory = (age: number): Task<string> =>
new Task(() => Promise.resolve(age > 18 ? "adult" : "minor"));
const userTask = getUser(1)
.flatMap(user => getAgeCategory(user.age))
.map(category => `User is ${category}`);
userTask.execute().then(console.log); // "User is adult"
Паттерны и лучшие практики
Для успешного применения ФП в TypeScript следуйте этим правилам:
- Избегайте побочных эффектов. Выносите ввод/вывод на границы системы (например, в обработчики событий или сервисы).
- Используйте алгебраические типы данных. Discriminated unions (размеченные объединения) идеально подходят для моделирования состояний.
- Применяйте паттерн "Pipeline". Компонуйте функции через
pipeилиcomposeдля читаемости. - Типизируйте ошиб