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

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

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

Начать курс

Что такое Effect-ts и зачем он нужен?

Effect-ts — это мощная библиотека для TypeScript, которая привносит в язык концепции функционального программирования, позволяя писать чистый, композируемый и типобезопасный код. Она решает одну из главных проблем в разработке на TypeScript (и JavaScript в целом) — управление побочными эффектами (side effects).

В традиционном императивном программировании побочные эффекты (работа с сетью, файлами, базами данных, генерация случайных чисел, работа с временем) разбросаны по всему коду, что делает его сложным для тестирования, отладки и анализа. Effect-ts предлагает элегантный подход: все побочные эффекты описываются как данные (значения), которые можно комбинировать, трансформировать и выполнять в контролируемой среде.

Основные преимущества Effect-ts:

  • Типобезопасность: Компилятор TypeScript проверяет типы ошибок и успешных результатов на уровне типов.
  • Композируемость: Эффекты легко комбинируются с помощью функций map, flatMap, zip и других.
  • Управление ошибками: Ошибки обрабатываются как часть типа эффекта, а не через исключения.
  • Тестируемость: Зависимости (например, HTTP-клиенты, базы данных) можно легко подменять.
  • Конкурентность: Встроенная поддержка асинхронности, параллелизма и отмены операций.
  • Предсказуемость: Код становится более декларативным и легче для понимания.

Effect-ts особенно полезен в крупных проектах, где важна надежность, тестируемость и масштабируемость. Он используется в бэкенд-разработке (Node.js), фронтенде (React) и даже в мобильных приложениях.

Установка

Для установки Effect-ts в ваш проект используйте npm или yarn:

npm install effect

или

yarn add effect

Также убедитесь, что у вас установлен TypeScript версии 4.7 или выше:

npm install typescript --save-dev

Для работы с Effect-ts вам понадобится настроить tsconfig.json, включив строгий режим (strict: true) и, желательно, target: "ES2020" или выше.

Быстрый старт с примером кода

Давайте напишем простую программу, которая считывает имя пользователя из командной строки и выводит приветствие. Это покажет базовую концепцию Effect-ts: описание эффекта и его выполнение.

import { Effect, Console } from "effect"

// Описываем эффект: запрос имени
const askName: Effect.Effect<string> = Effect.sync(() => {
  // В реальном приложении здесь был бы ввод с клавиатуры
  return "Алексей"
})

// Описываем эффект: вывод приветствия
const greet = (name: string): Effect.Effect<void> =>
  Console.log(`Привет, ${name}!`)

// Комбинируем эффекты
const program: Effect.Effect<void> = askName.pipe(
  Effect.flatMap(greet)
)

// Запускаем программу
Effect.runSync(program)
// Вывод: Привет, Алексей!

В этом примере:

  • Effect.sync — создает эффект из синхронной функции.
  • Console.log — это эффект для вывода в консоль.
  • pipe — позволяет последовательно применять функции к значению.
  • flatMap — используется для последовательного выполнения эффектов.
  • Effect.runSync — выполняет эффект синхронно (только для синхронных эффектов).

Основные методы и функции Effect-ts

1. Effect.sync

Что делает: Создает эффект, который оборачивает синхронную функцию. Функция выполняется лениво (только при запуске эффекта).

Параметры:

  • f: () => A — синхронная функция, возвращающая значение типа A.

Пример:

import { Effect } from "effect"

const getRandomInt: Effect.Effect<number> = Effect.sync(() => {
  return Math.floor(Math.random() * 100)
})

const result = Effect.runSync(getRandomInt)
console.log(result) // Вывод: случайное число от 0 до 99

2. Effect.async

Что делает: Создает эффект из асинхронной операции (например, callback-based API). Позволяет интегрировать существующий асинхронный код.

Параметры:

  • register: (resume: (effect: Effect<A, E>) => void) => void — функция, которая получает функцию resume для завершения эффекта.

Пример:

import { Effect } from "effect"

const readFileAsync = (path: string): Effect.Effect<string> =>
  Effect.async<string>((resume) => {
    // Имитация асинхронного чтения файла
    setTimeout(() => {
      resume(Effect.succeed(`Содержимое файла: ${path}`))
    }, 1000)
  })

const program = readFileAsync("data.txt")
const result = await Effect.runPromise(program)
console.log(result) // Вывод: Содержимое файла: data.txt

3. Effect.map

Что делает: Трансформирует успешное значение эффекта с помощью функции. Аналог Array.map для эффектов.

Параметры:

  • f: (a: A) => B — функция преобразования значения.

Пример:

import { Effect } from "effect"

const getNumber: Effect.Effect<number> = Effect.sync(() => 42)
const toString: Effect.Effect<string> = getNumber.pipe(
  Effect.map((n) => `Число: ${n}`)
)

console.log(Effect.runSync(toString)) // Вывод: Число: 42

4. Effect.flatMap

Что делает: Последовательно выполняет два эффекта, где второй зависит от результата первого. Ключевая функция для композиции эффектов.

Параметры:

  • f: (a: A) => Effect<B, E, R> — функция, принимающая значение и возвращающая новый эффект.

Пример:

import { Effect, Console } from "effect"

const getUserName: Effect.Effect<string> = Effect.sync(() => "Иван")
const saveToDatabase = (name: string): Effect.Effect<void> =>
  Console.log(`Сохранено: ${name}`)

const program = getUserName.pipe(
  Effect.flatMap((name) => saveToDatabase(name))
)

Effect.runSync(program) // Вывод: Сохранено: Иван

5. Effect.zip

Что делает: Объединяет два эффекта в один, возвращая кортеж из результатов. Выполняет эффекты последовательно.

Параметры:

  • that: Effect<B, E, R> — второй эффект.
  • Опционально: options?: { concurrent?: boolean } — для параллельного выполнения.

Пример:

import { Effect } from "effect"

const getX: Effect.Effect<number> = Effect.sync(() => 10)
const getY: Effect.Effect<number> = Effect.sync(() => 20)

const combined: Effect.Effect<[number, number]> = Effect.zip(getX, getY)
const [x, y] = Effect.runSync(combined)
console.log(x, y) // Вывод: 10 20

6. Effect.all

Что делает: Объединяет массив или объект эффектов в один эффект, возвращающий массив или объект результатов. По умолчанию выполняется последовательно.

Параметры:

  • effects: Iterable<Effect<A, E, R>> | Record<string, Effect<A, E, R>> — коллекция эффектов.
  • Опционально: options?: { concurrency?: number | "unbounded" } — уровень параллелизма.

Пример:

import { Effect } from "effect"

const effects = [
  Effect.sync(() => 1),
  Effect.sync(() => 2),
  Effect.sync(() => 3),
]

const allNumbers: Effect.Effect<number[]> = Effect.all(effects)
console.log(Effect.runSync(allNumbers)) // Вывод: [1, 2, 3]

7. Effect.catchAll

Что делает: Обрабатывает ошибки эффекта, предоставляя альтернативный эффект для восстановления.

Параметры:

  • handler: (e: E) => Effect<A, E2, R> — функция обработки ошибки.

Пример:

import { Effect, Console } from "effect"

const riskyOperation: Effect.Effect<number, string> = Effect.sync(() => {
  throw new Error("Что-то пошло не так")
})

const safeOperation = riskyOperation.pipe(
  Effect.catchAll((error) => {
    Console.log(`Ошибка: ${error}`)
    return Effect.succeed(0) // Возвращаем значение по умолчанию
  })
)

console.log(Effect.runSync(safeOperation)) // Вывод: Ошибка: Что-то пошло не так \n 0

8. Effect.tryCatch

Что делает: Создает эффект, который перехватывает исключения из синхронной функции и преобразует их в ошибку эффекта.

Параметры:

  • attempt: () => A — функция, которая может выбросить исключение.
  • catch: (u: unknown) => E — функция преобразования исключения в ошибку.

Пример:

import { Effect } from "effect"

const parseJson = (json: string): Effect.Effect<unknown, Error> =>
  Effect.tryCatch(
    () => JSON.parse(json),
    (error) => new Error(`Ошибка парсинга: ${error}`)
  )

const validJson = parseJson('{"name": "Анна"}')
console.log(Effect.runSync(validJson)) // Вывод: { name: 'Анна' }

const invalidJson = parseJson("не json")
console.log(Effect.runSync(invalidJson)) // Ошибка: Error: Ошибка парсинга: ...

9. Effect.retry

Что делает: Повторяет выполнение эффекта при возникновении ошибки. Можно настроить количество попыток и стратегию ожидания.

Параметры:

  • policy: Schedule<A, E, Out> — расписание повторных попыток.

Пример:

import { Effect, Schedule, Console } from "effect"

let attempts = 0
const unstableOperation: Effect.Effect<string> = Effect.sync(() => {
  attempts++
  if (attempts < 3) {
    throw new Error("Временная ошибка")
  }
  return "Успех!"
})

const program = unstableOperation.pipe(
  Effect.retry(Schedule.recurs(3)), // Повторить до 3 раз
  Effect.catchAll((e) => {
    Console.log(`Не удалось после всех попыток: ${                    

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