fp-ts для TypeScript: полное руководство с примерами

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

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

Начать курс

fp-ts для TypeScript: полное руководство по функциональному программированию

1. Что это и зачем нужна

fp-ts (Functional Programming in TypeScript) — это мощная библиотека с открытым исходным кодом, которая привносит в TypeScript парадигму функционального программирования (ФП). Она предоставляет набор типов, функций и комбинаторов, вдохновлённых такими языками, как Haskell, PureScript и Scala. В отличие от императивного или объектно-ориентированного подхода, fp-ts позволяет писать код, который является чистым, предсказуемым и легко тестируемым. Библиотека построена на концепциях функторов, монад, аппликативных функторов и других абстракций, которые помогают управлять побочными эффектами, обрабатывать ошибки и работать с асинхронностью без исключений и мутаций состояния.

Зачем использовать fp-ts? В современной разработке на TypeScript часто возникают проблемы: неожиданные исключения (например, при работе с null или undefined), сложность тестирования из-за скрытых зависимостей и побочных эффектов, а также запутанный асинхронный код с колбэками. fp-ts решает эти проблемы, предлагая строгую систему типов и композируемые абстракции. Например, тип Option явно моделирует отсутствие значения, а Either — обработку ошибок без try/catch. Это делает код более безопасным и документированным на уровне типов.

Библиотека особенно полезна в проектах с высокой сложностью бизнес-логики, где важна надёжность и поддерживаемость. Она также отлично подходит для команд, которые хотят внедрить функциональные принципы постепенно, не переписывая весь проект. fp-ts активно поддерживается сообществом и имеет богатую экосистему, включая интеграцию с io-ts (валидация данных), monocle-ts (линзы) и hyper-ts (веб-серверы).

2. Установка

fp-ts распространяется через npm. Для установки выполните команду:

npm install fp-ts

Также рекомендуется установить @types/node для поддержки типов Node.js, если вы работаете не в браузере:

npm install -D @types/node

Для работы с fp-ts требуется TypeScript версии 4.0 или выше. Убедитесь, что в вашем tsconfig.json включена опция "strict": true для максимальной безопасности типов.

3. Быстрый старт — минимальный рабочий пример

Рассмотрим простейший пример: безопасное деление двух чисел с обработкой деления на ноль. Без fp-ts мы бы написали:

function divide(a: number, b: number): number | null {
  if (b === 0) return null;
  return a / b;
}

const result = divide(10, 2);
if (result !== null) {
  console.log(result); // 5
}

С fp-ts используем тип Option:

import { pipe } from 'fp-ts/function';
import { Option, some, none, fromPredicate } from 'fp-ts/Option';

const safeDivide = (a: number, b: number): Option =>
  pipe(
    b,
    fromPredicate((x) => x !== 0),
    Option.map((divisor) => a / divisor)
  );

const result = safeDivide(10, 2);
// result: Option — some(5) или none

// Извлекаем значение
import { fold } from 'fp-ts/Option';

const message = pipe(
  result,
  fold(
    () => 'Ошибка: деление на ноль',
    (value) => `Результат: ${value}`
  )
);
console.log(message); // "Результат: 5"

Этот пример демонстрирует ключевые принципы fp-ts: композиция через pipe, явное моделирование отсутствия значения и обработка ошибок без исключений.

4. Основные методы/функции/классы

4.1. Option — работа с опциональными значениями

Сигнатура:

// Конструкторы
declare const some: (a: A) => Option;
declare const none: Option;

// Основные методы
declare const map: (f: (a: A) => B) => (fa: Option) => Option;
declare const chain: (f: (a: A) => Option) => (fa: Option) => Option;
declare const fold: (onNone: () => B, onSome: (a: A) => B) => (fa: Option) => B;

Что делает: Тип Option представляет значение, которое может отсутствовать. some создаёт присутствующее значение, none — отсутствующее. map применяет функцию к внутреннему значению, если оно есть; chain позволяет последовательно вызывать функции, возвращающие Option; fold — это паттерн-матчинг для обработки обоих случаев.

Пример:

import { pipe } from 'fp-ts/function';
import { Option, some, none, map, chain, fold } from 'fp-ts/Option';

const getUserName = (id: number): Option => {
  const users: Record = { 1: 'Alice', 2: 'Bob' };
  return id in users ? some(users[id]) : none;
};

const getGreeting = (name: string): Option =>
  name.length > 0 ? some(`Hello, ${name}!`) : none;

const result = pipe(
  getUserName(1),
  chain(getGreeting),
  fold(
    () => 'User not found or invalid name',
    (greeting) => greeting
  )
);
console.log(result); // "Hello, Alice!"

4.2. Either — обработка ошибок без исключений

Сигнатура:

declare const right: (a: A) => Either;
declare const left: (e: E) => Either;
declare const map: (f: (a: A) => B) => (fa: Either) => Either;
declare const chain: (f: (a: A) => Either) => (fa: Either) => Either;
declare const fold: (onLeft: (e: E) => B, onRight: (a: A) => B) => (fa: Either) => B;

Что делает: Either — это тип для представления одного из двух возможных значений: left (обычно ошибка) или right (успех). В отличие от исключений, ошибки становятся частью сигнатуры функции, что делает код более предсказуемым.

Пример:

import { pipe } from 'fp-ts/function';
import { Either, right, left, map, chain, fold } from 'fp-ts/Either';

type ValidationError = { message: string };

const validateAge = (age: number): Either =>
  age >= 0 && age <= 120 ? right(age) : left({ message: 'Invalid age' });

const validateName = (name: string): Either =>
  name.length > 0 ? right(name) : left({ message: 'Name cannot be empty' });

const createUser = (name: string, age: number): Either =>
  pipe(
    validateName(name),
    chain((validName) =>
      pipe(
        validateAge(age),
        map((validAge) => `User: ${validName}, Age: ${validAge}`)
      )
    )
  );

const result = createUser('Alice', 25);
console.log(
  pipe(
    result,
    fold(
      (error) => `Error: ${error.message}`,
      (success) => success
    )
  )
); // "User: Alice, Age: 25"

4.3. Task — ленивые асинхронные вычисления

Сигнатура:

declare const of: (a: A) => Task;
declare const map: (f: (a: A) => B) => (fa: Task) => Task;
declare const chain: (f: (a: A) => Task) => (fa: Task) => Task;
declare const delay: (milliseconds: number) => (fa: Task) => Task;

Что делает: Task представляет асинхронное вычисление, которое никогда не завершается с ошибкой. Это аналог Promise, но ленивый: вычисление начинается только при вызове (). Идеально для композиции асинхронных операций.

Пример:

import { pipe } from 'fp-ts/function';
import { Task, of, map, chain, delay } from 'fp-ts/Task';

const fetchUser = (id: number): Task =>
  of(`User ${id}`); // имитация асинхронного запроса

const fetchPosts = (user: string): Task =>
  of([`Post 1 by ${user}`, `Post 2 by ${user}`]);

const program = pipe(
  fetchUser(1),
  delay(1000), // симуляция задержки
  chain(fetchPosts),
  map((posts) => posts.join(', '))
);

// Запуск вычисления
program().then(console.log); // Через 1 секунду: "Post 1 by User 1, Post 2 by User 1"

4.4. TaskEither — асинхронные вычисления с обработкой ошибок

Сигнатура:

declare const right: (a: A) => TaskEither;
declare const left: (e: E) => TaskEither;
declare const map: (f: (a: A) => B) => (fa: TaskEither) => TaskEither;
declare const chain: (f: (a: A) => TaskEither) => (fa: TaskEither) => TaskEither;
declare const fold: (onLeft: (e: E) => Task, onRight: (a: A) => Task) => (fa: TaskEither) => Task;

Что делает: TaskEither объединяет возможности Task (ленивая асинхронность) и Either (обработка ошибок). Это основной тип для работы с асинхронными операциями, которые могут завершиться ошибкой.

Пример:

import { pipe } from 'fp-ts/function';
import { TaskEither, right, left, map, chain, fold } from 'fp-ts/TaskEither';
import { Task, of } from 'fp-ts/Task';

type ApiError = { status: number; message: string };

const fetchFromApi = (url: string): TaskEither => {
  if (url === '/valid') return right('Data from API');
  return left({ status: 404, message: 'Not Found' });
};

const processData = (data: string): TaskEither =>
  right(data.length);

const program = pipe(
  fetchFromApi('/valid'),
  chain(processData),
  fold(
    (error) => of(`Error ${error.status}: ${error.message}`),
    (length) => of(`Data length: ${length}`)
  )
);

program().then(console.log); // "Data length: 12"

4.5. Reader — внедрение зависимостей

Сигнатура:

declare const ask: () => Reader;
declare const asks: (f: (r: R) => A) => Reader;
declare const map: (f: (a: A) => B) => (fa: Reader) => Reader;
declare const chain: (f: (a: A) => Reader) => (fa: Reader) => Reader;

Что делает: Reader — это вычисление, которое зависит от некоторого окружения (контекста). Позволяет реализовать внедрение зависимостей (dependency injection) без классов и мутаций. Вычисление выполняется только когда предоставлено окружение.

Пример:

import { pipe } from 'fp-ts/function';
import { Reader, ask, map, chain } from 'fp-ts/Reader';

interface Config {
  apiUrl: string;
  timeout: number;
}

const getApiUrl: Reader = pipe(
  ask(),
  map((config) => config.apiUrl)
);

const fetchData = (url: string): Reader =>
  pipe(
    ask(),
    map((config) => `Fetching from ${url} with timeout ${config.timeout}ms`)
  );

const program = pipe(
  getApiUrl,
  chain(fetchData)
);

const config: Config = { apiUrl: 'https://api.example.com', timeout: 5000 };
console.log(program(config)); // "Fetching from https://api.example.com with timeout 5000ms"

4.6. State — управление состоянием

Сигнатура:

declare const get: () => State;
declare const put: (s: S) => State;
declare const modify: (f: (s: S) => S) => State;
declare const map: (f: (a: A) => B) => (fa: State) => State;
declare const chain: (f: (a: A) => State) => (fa: State) => State;

Что делает: State представляет вычисление, которое читает и изменяет некоторое состояние. Возвращает пару (результат, новое состояние). Позволяет моделировать мутации в чистом функциональном стиле.

Пример:

import { pipe } from 'fp-ts/function';
import {                    

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