Функциональное программирование в TypeScript: продвинутый уровень

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Функциональное программирование в 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 для читаемости.
  • Типизируйте ошиб

Блоги

Рекомендаций по книгам