Функциональное программирование в TypeScript: от А до Я
Функциональное программирование (ФП) — это парадигма, которая рассматривает вычисления как оценку математических функций и избегает изменения состояния и изменяемых данных. TypeScript, будучи строго типизированным расширением JavaScript, предоставляет отличную основу для применения принципов ФП, позволяя писать более предсказуемый, тестируемый и надежный код. В этой статье мы разберем ключевые концепции ФП в TypeScript, начиная с основ и заканчивая продвинутыми техниками.
Основные принципы функционального программирования
Прежде чем углубляться в код, важно понять фундаментальные принципы, на которых строится ФП.
Чистые функции (Pure Functions)
Чистая функция — это функция, которая:
- Всегда возвращает один и тот же результат для одних и тех же аргументов (детерминированность).
- Не имеет побочных эффектов (не изменяет внешние переменные, не выводит данные в консоль, не отправляет запросы).
Пример чистой функции:
function add(a: number, b: number): number {
return a + b;
}
// Всегда предсказуемо
console.log(add(2, 3)); // 5
console.log(add(2, 3)); // 5
Пример нечистой функции:
let counter = 0;
function increment(): number {
return ++counter; // Побочный эффект: изменение внешней переменной
}
Неизменяемость (Immutability)
Данные не должны изменяться после создания. Вместо изменения существующего объекта мы создаем его новую копию с необходимыми изменениями.
interface User {
name: string;
age: number;
}
const user: User = { name: "Alice", age: 30 };
// Плохо (мутация)
user.age = 31;
// Хорошо (иммутабельность)
const updatedUser: User = { ...user, age: 31 };
console.log(user); // { name: "Alice", age: 30 } - оригинал не изменился
console.log(updatedUser); // { name: "Alice", age: 31 }
Композиция и каррирование в TypeScript
Композиция функций — это процесс объединения нескольких простых функций для создания более сложной. Каррирование (currying) — это трансформация функции с несколькими аргументами в последовательность функций, каждая из которых принимает один аргумент.
Композиция (Function Composition)
Типизированная реализация композиции в TypeScript:
function compose<A, B, C>(f: (x: B) => C, g: (x: A) => B): (x: A) => C {
return (x: A) => f(g(x));
}
const toUpperCase = (str: string): string => str.toUpperCase();
const exclaim = (str: string): string => `${str}!`;
const shout = compose(exclaim, toUpperCase);
console.log(shout("hello world")); // "HELLO WORLD!"
Каррирование (Currying)
Пример каррированной функции:
function multiply(a: number): (b: number) => number {
return (b: number) => a * b;
}
const double = multiply(2);
const triple = multiply(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
Каррирование делает функции более гибкими и удобными для композиции.
Работа с монадами: Option и Result
Монады — это мощный паттерн для работы с побочными эффектами и обработкой ошибок без нарушения чистоты функций. В TypeScript часто используются монады Option (или Maybe) и Result (или Either).
Монада Option (Maybe)
Позволяет безопасно работать со значениями, которые могут быть null или undefined.
type Option<T> = T | null;
function safeDivide(a: number, b: number): Option<number> {
return b === 0 ? null : a / b;
}
function getValue(value: Option<number>): string {
if (value === null) {
return "Ошибка: деление на ноль";
}
return `Результат: ${value}`;
}
console.log(getValue(safeDivide(10, 2))); // "Результат: 5"
console.log(getValue(safeDivide(10, 0))); // "Ошибка: деление на ноль"
Монада Result (Either)
Используется для явной обработки ошибок, возвращая либо успешный результат, либо ошибку.
type Result<T, E> = { success: true; value: T } | { success: false; error: E };
function parseJSON(jsonString: string): Result<any, string> {
try {
const parsed = JSON.parse(jsonString);
return { success: true, value: parsed };
} catch (e) {
return { success: false, error: "Некорректный JSON" };
}
}
const result = parseJSON('{"name": "Bob"}');
if (result.success) {
console.log(result.value); // { name: "Bob" }
} else {
console.error(result.error);
}
Паттерны функционального программирования на практике
Рассмотрим несколько практических примеров, демонстрирующих преимущества ФП.
Обработка массивов
Вместо циклов for и мутаций используем методы map, filter и reduce:
const numbers: number[] = [1, 2, 3, 4, 5, 6];
// Трансформация
const doubled = numbers.map(n => n * 2); // [2, 4, 6, 8, 10, 12]
// Фильтрация
const evens = numbers.filter(n => n % 2 === 0); // [2, 4, 6]
// Агрегация
const sum = numbers.reduce((acc, n) => acc + n, 0); // 21
// Комбинация
const result2 = numbers
.filter(n => n % 2 === 0)
.map(n => n * 10)
.reduce((acc, n) => acc + n, 0); // 120
Функции высшего порядка
Функции, которые принимают другие функции в качестве аргументов или возвращают их:
function withLogging<T>(fn: (...args: any[]) => T): (...args: any[]) =>