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

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

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

Начать курс

Функциональное программирование в 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,                

Блоги

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