TypeScript Effect システム:Effect-TSで副作用をエレガントに処理する

前端工程

なぜ Effect-TS が TypeScript のエラー処理を革新しているのか

TypeScript プロジェクトにおいて、非同期エラー処理は常に課題でした。try/catch は非同期エラーをキャッチできず、Promise.catch() は見落としやすく、async/await はエラー処理を暗黙的にします。Effect-TS は副作用を値としてモデル化することで、この状況を根本的に変えました。

特徴 Effect-TS Promise/async-await fp-ts neverthrow
型安全なエラー ✅ 完全に型付け ❌ unknown ✅ 完全に型付け ✅ 完全に型付け
依存性注入 ✅ 組み込み Layer ❌ なし ❌ 手動必要 ❌ なし
並行プリミティブ ✅ Fiber ❌ ネイティブなし ❌ なし ❌ なし
リトライ/タイムアウト ✅ 組み込み ❌ 手動 ❌ 手動 ❌ 手動
合成可能性 ✅ 高い ⚠️ 中程度 ✅ 高い ⚠️ 中程度
学習曲線 ⚠️ 急 ✅ 低い ❌ 急 ✅ 低い
エコシステム成熟度 ✅ 活発 ✅ ネイティブ ⚠️ メンテナンスモード ⚠️ ニッチ
リソース解放 ✅ Scope ❌ finally ❌ なし ❌ なし

Effect-TS は 2026 年、TypeScript の関数型プログラミングにおけるデファクトスタンダードとなりました。その核心思想は:副作用をデータとして記述し、即時実行しない。これにより、プログラムはテスト可能、合成可能、推論可能になります。


Effect:コアコンセプトと基本操作

Effect は Effect-TS のコア抽象であり、失敗する可能性のある計算を記述します。「強化版 Promise」と考えてください — ただし、遅延評価で、型安全で、合成可能です。

Effect の作成

import { Effect } from "effect";

// 同期値から作成
const syncEffect = Effect.succeed(42);

// 例外を投げる可能性のある同期コードから作成
const riskySync = Effect.sync(() => {
  const data = JSON.parse('{"name":"effect"}');
  return data;
});

// 非同期操作から作成
const asyncEffect = Effect.tryPromise({
  try: () => fetch("https://api.example.com/users"),
  catch: (error) => new FetchError(error),
});

// カスタムエラー型の定義
class FetchError {
  readonly _tag = "FetchError";
  constructor(readonly cause: unknown) {}
}

class ParseError {
  readonly _tag = "ParseError";
  constructor(readonly cause: unknown) {}
}

チェーン操作とマッピング

import { Effect } from "effect";

const program = Effect.gen(function* (_) {
  const response = yield* _(
    Effect.tryPromise({
      try: () => fetch("https://api.example.com/users"),
      catch: (error) => new FetchError(error),
    })
  );

  const data = yield* _(
    Effect.try({
      try: () => response.json(),
      catch: (error) => new ParseError(error),
    })
  );

  return data;
});

// マッピングとチェーン操作
const transformed = pipe(
  Effect.succeed(10),
  Effect.map((n) => n * 2),
  Effect.flatMap((n) => Effect.succeed(n + 5))
);

Effect の実行

import { Effect } from "effect";

const program = Effect.succeed("Hello, Effect-TS!");

// Node.js で実行
Effect.runPromise(program).then(console.log);
// 出力: Hello, Effect-TS!

// 同期実行(同期 Effect のみ)
Effect.runSync(Effect.succeed(42));
// 出力: 42

// コールバックで実行
Effect.runCallback(program, {
  onExit: (exit) => {
    if (exit._tag === "Success") {
      console.log(exit.value);
    } else {
      console.error(exit.cause);
    }
  },
});

Layer:依存性注入システム

Layer は Effect-TS の依存性注入メカニズムであり、サービスの作成と使用を完全に分離できます。これはテストとモジュール化において非常に強力です。

import { Effect, Layer, Context } from "effect";

// サービスインターフェースの定義
class Database extends Context.Tag("Database")<
  Database,
  {
    readonly query: (sql: string) => Effect.Effect<unknown[], DbError>;
    readonly connect: () => Effect.Effect<void, DbError>;
  }
>() {}

class DbError {
  readonly _tag = "DbError";
  constructor(readonly message: string) {}
}

// PostgreSQL Layer の実装
const PostgresLive = Layer.succeed(Database, {
  query: (sql: string) =>
    Effect.tryPromise({
      try: () => pgPool.query(sql),
      catch: (error) => new DbError(String(error)),
    }),
  connect: () =>
    Effect.tryPromise({
      try: () => pgPool.connect(),
      catch: (error) => new DbError(String(error)),
    }),
});

// Mock Layer(テスト用)
const DatabaseMock = Layer.succeed(Database, {
  query: (sql: string) => Effect.succeed([{ id: 1, name: "test" }]),
  connect: () => Effect.succeed(void 0),
});

// サービスの使用
const getUserById = (id: number) =>
  Effect.gen(function* (_) {
    const db = yield* _(Database);
    const rows = yield* _(db.query(`SELECT * FROM users WHERE id = ${id}`));
    return rows[0];
  });

// 本番環境はリアル Layer を使用
const program = getUserById(1).pipe(
  Effect.provide(PostgresLive)
);

// テスト環境は Mock Layer を使用
const testProgram = getUserById(1).pipe(
  Effect.provide(DatabaseMock)
);

Service:サービス定義と合成

Service は Effect-TS で注入可能なサービスを定義する標準パターンで、Context.Tag を通じて実装されます。

import { Effect, Context, Layer } from "effect";

// Logger サービスの定義
class Logger extends Context.Tag("Logger")<
  Logger,
  {
    readonly info: (msg: string) => Effect.Effect<void>;
    readonly error: (msg: string) => Effect.Effect<void>;
  }
>() {}

// Config サービスの定義
class AppConfig extends Context.Tag("AppConfig")<
  AppConfig,
  {
    readonly apiUrl: string;
    readonly timeout: number;
  }
>() {}

// Logger の実装
const LoggerLive = Layer.succeed(Logger, {
  info: (msg) => Effect.sync(() => console.log(`[INFO] ${msg}`)),
  error: (msg) => Effect.sync(() => console.error(`[ERROR] ${msg}`)),
});

// Config の実装
const ConfigLive = Layer.succeed(AppConfig, {
  apiUrl: "https://api.example.com",
  timeout: 5000,
});

// 複数サービスの合成
const AppLive = Layer.merge(LoggerLive, ConfigLive);

// 複数サービスを使用するビジネスロジック
const fetchWithLogging = Effect.gen(function* (_) {
  const logger = yield* _(Logger);
  const config = yield* _(AppConfig);

  yield* _(logger.info(`Fetching from ${config.apiUrl}`));

  const result = yield* _(
    Effect.tryPromise({
      try: () => fetch(config.apiUrl),
      catch: (error) => new FetchError(error),
    })
  );

  yield* _(logger.info("Fetch completed"));
  return result;
}).pipe(Effect.provide(AppLive));

Fiber:並行プログラミング

Fiber は Effect-TS の軽量並行ユニットで、コルーチンに似ていますがより強力で、中断、監視、構造化並行をサポートしています。

import { Effect, Fiber } from "effect";

// 複数の Effect を並列実行
const parallelExample = Effect.gen(function* (_) {
  const [users, posts, comments] = yield* _(
    Effect.all(
      [
        Effect.tryPromise({
          try: () => fetch("/api/users"),
          catch: (e) => new FetchError(e),
        }),
        Effect.tryPromise({
          try: () => fetch("/api/posts"),
          catch: (e) => new FetchError(e),
        }),
        Effect.tryPromise({
          try: () => fetch("/api/comments"),
          catch: (e) => new FetchError(e),
        }),
      ],
      { concurrency: "unbounded" }
    )
  );

  return { users, posts, comments };
});

// レース:最速の結果を取得
const raceExample = Effect.gen(function* (_) {
  const result = yield* _(
    Effect.race(
      Effect.tryPromise({
        try: () => fetch("/api/cdn1/data"),
        catch: (e) => new FetchError(e),
      }),
      Effect.tryPromise({
        try: () => fetch("/api/cdn2/data"),
        catch: (e) => new FetchError(e),
      })
    )
  );
  return result;
});

// 手動 Fiber 制御
const fiberExample = Effect.gen(function* (_) {
  const fiber = yield* _(
    Effect.fork(
      Effect.tryPromise({
        try: () => longRunningTask(),
        catch: (e) => new TaskError(e),
      })
    )
  );

  // 他の処理...
  yield* _(Effect.sleep("2 seconds"));

  // Fiber の完了を待機
  const result = yield* _(Fiber.join(fiber));
  return result;
});

// 構造化並行:親 Effect 終了時に子 Fiber が自動中断
const structuredConcurrency = Effect.gen(function* (_) {
  yield* _(
    Effect.fork(
      Effect.gen(function* (_) {
        yield* _(Effect.sleep("10 seconds"));
        console.log("これは実行されない(親 Effect が先に終了するため)");
      })
    )
  );

  yield* _(Effect.sleep("1 second"));
  // 親 Effect 終了、子 Fiber は自動中断
});

コールバックから Promise へ、そして Effect へ:完全移行例

コールバック地獄の時代

function getUserCallback(id: number, callback: (err: Error | null, user?: User) => void) {
  db.query("SELECT * FROM users WHERE id = ?", [id], (err, rows) => {
    if (err) return callback(err);
    if (rows.length === 0) return callback(new Error("User not found"));
    callback(null, rows[0]);
  });
}

function getOrdersCallback(userId: number, callback: (err: Error | null, orders?: Order[]) => void) {
  db.query("SELECT * FROM orders WHERE userId = ?", [userId], (err, rows) => {
    if (err) return callback(err);
    callback(null, rows);
  });
}

// 使用:コールバック地獄
getUserCallback(1, (err, user) => {
  if (err) { console.error(err); return; }
  getOrdersCallback(user!.id, (err, orders) => {
    if (err) { console.error(err); return; }
    console.log({ user, orders });
  });
});

Promise/async-await の時代

async function getUser(id: number): Promise<User> {
  const rows = await db.query("SELECT * FROM users WHERE id = ?", [id]);
  if (rows.length === 0) throw new Error("User not found");
  return rows[0];
}

async function getOrders(userId: number): Promise<Order[]> {
  return db.query("SELECT * FROM orders WHERE userId = ?", [userId]);
}

// 使用:エラー型が失われる
async function main() {
  try {
    const user = await getUser(1);
    const orders = await getOrders(user.id);
    console.log({ user, orders });
  } catch (error) {
    // error は unknown、ユーザー不在とDBエラーを区別できない
    console.error(error);
  }
}

Effect-TS の時代

class UserNotFoundError {
  readonly _tag = "UserNotFoundError";
  constructor(readonly userId: number) {}
}

class DbQueryError {
  readonly _tag = "DbQueryError";
  constructor(readonly message: string, readonly cause: unknown) {}
}

const getUser = (id: number) =>
  Effect.gen(function* (_) {
    const db = yield* _(Database);
    const rows = yield* _(
      db.query("SELECT * FROM users WHERE id = ?", [id]).pipe(
        Effect.mapError((e) => new DbQueryError("Query failed", e))
      )
    );
    if (rows.length === 0) {
      return yield* _(Effect.fail(new UserNotFoundError(id)));
    }
    return rows[0] as User;
  });

const getOrders = (userId: number) =>
  Effect.gen(function* (_) {
    const db = yield* _(Database);
    return yield* _(db.query("SELECT * FROM orders WHERE userId = ?", [userId]));
  });

// 使用:完全に型安全なエラー処理
const program = Effect.gen(function* (_) {
  const user = yield* _(getUser(1));
  const orders = yield* _(getOrders(user.id));
  return { user, orders };
}).pipe(
  Effect.catchTag("UserNotFoundError", (e) =>
    Effect.succeed({ error: `ユーザー ${e.userId} が見つかりません` })
  ),
  Effect.catchTag("DbQueryError", (e) =>
    Effect.succeed({ error: `データベースエラー: ${e.message}` })
  )
);

エラー処理パターン

catchAll:すべてのエラーをキャッチ

import { Effect } from "effect";

const resilient = pipe(
  riskyOperation(),
  Effect.catchAll((error) =>
    Effect.succeed(`エラーから復旧: ${String(error)}`)
  )
);

catchTag:型ごとにエラーをキャッチ

import { Effect } from "effect";

const taggedHandler = pipe(
  program,
  Effect.catchTag("FetchError", (e) =>
    Effect.succeed({ fallback: true, reason: e.cause })
  ),
  Effect.catchTag("ParseError", (e) =>
    Effect.succeed({ fallback: true, reason: e.cause })
  )
);

retry:指数バックオフ

import { Effect, Schedule } from "effect";

const retryWithBackoff = pipe(
  fetchFromApi(),
  Effect.retry(
    Schedule.exponential("1 second").pipe(
      Schedule.compose(Schedule.recurs(5)),
      Schedule.tap((attempt) =>
        Effect.sync(() => console.log(`リトライ ${attempt} 回目...`))
      )
    )
  )
);

// 条件付きリトライ
const conditionalRetry = pipe(
  fetchFromApi(),
  Effect.retry(
    Schedule.exponential("500 millis").pipe(
      Schedule.whileOutput((delay) => delay < 30_000),
      Schedule.whileInput((error) => error._tag === "NetworkError")
    )
  )
);

timeout:タイムアウト制御

import { Effect } from "effect";

class TimeoutError {
  readonly _tag = "TimeoutError";
}

const withTimeout = pipe(
  longRunningOperation(),
  Effect.timeout("5 seconds"),
  Effect.mapError(() => new TimeoutError())
);

// タイムアウトとフォールバック
const withTimeoutFallback = pipe(
  longRunningOperation(),
  Effect.timeoutFail({
    duration: "3 seconds",
    onTimeout: () => new TimeoutError(),
  }),
  Effect.catchTag("TimeoutError", () =>
    Effect.succeed(getCachedResult())
  )
);

依存性注入の実践:完全な例

import { Effect, Context, Layer } from "effect";

// エラー型の定義
class HttpError {
  readonly _tag = "HttpError";
  constructor(readonly status: number, readonly message: string) {}
}

// HttpClient サービスの定義
class HttpClient extends Context.Tag("HttpClient")<
  HttpClient,
  {
    readonly get: (url: string) => Effect.Effect<Response, HttpError>;
    readonly post: (url: string, body: unknown) => Effect.Effect<Response, HttpError>;
  }
>() {}

// Cache サービスの定義
class Cache extends Context.Tag("Cache")<
  Cache,
  {
    readonly get: (key: string) => Effect.Effect<unknown | null>;
    readonly set: (key: string, value: unknown, ttl: number) => Effect.Effect<void>;
  }
>() {}

// リアル HttpClient 実装
const HttpClientLive = Layer.succeed(HttpClient, {
  get: (url) =>
    Effect.tryPromise({
      try: () => fetch(url),
      catch: (error) => new HttpError(0, String(error)),
    }).pipe(
      Effect.flatMap((res) =>
        res.ok
          ? Effect.succeed(res)
          : Effect.fail(new HttpError(res.status, res.statusText))
      )
    ),
  post: (url, body) =>
    Effect.tryPromise({
      try: () =>
        fetch(url, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(body),
        }),
      catch: (error) => new HttpError(0, String(error)),
    }).pipe(
      Effect.flatMap((res) =>
        res.ok
          ? Effect.succeed(res)
          : Effect.fail(new HttpError(res.status, res.statusText))
      )
    ),
});

// インメモリ Cache 実装
const InMemoryCacheLive = Layer.effect(
  Cache,
  Effect.sync(() => {
    const store = new Map<string, { value: unknown; expires: number }>();
    return {
      get: (key) =>
        Effect.sync(() => {
          const entry = store.get(key);
          if (!entry || entry.expires < Date.now()) return null;
          return entry.value;
        }),
      set: (key, value, ttl) =>
        Effect.sync(() => {
          store.set(key, { value, expires: Date.now() + ttl });
        }),
    };
  })
);

// ビジネスロジック:キャッシュ付き API 呼び出し
const fetchWithCache = (url: string) =>
  Effect.gen(function* (_) {
    const cache = yield* _(Cache);
    const http = yield* _(HttpClient);

    const cached = yield* _(cache.get(url));
    if (cached) return cached;

    const response = yield* _(http.get(url));
    const data = yield* _(
      Effect.try({
        try: () => response.json(),
        catch: (e) => new HttpError(0, `Parse error: ${String(e)}`),
      })
    );

    yield* _(cache.set(url, data, 60_000));
    return data;
  });

// アプリケーションの組み立て
const AppLayer = Layer.merge(HttpClientLive, InMemoryCacheLive);

const main = fetchWithCache("https://api.example.com/data").pipe(
  Effect.provide(AppLayer),
  Effect.catchTag("HttpError", (e) =>
    Effect.succeed({ error: `HTTP ${e.status}: ${e.message}` })
  )
);

並行の実践:完全な Fiber 例

import { Effect, Fiber, Ref, Queue } from "effect";

// プロデューサー・コンシューマーパターン
const producerConsumer = Effect.gen(function* (_) {
  const queue = yield* _(Queue.bounded<string>(10));

  const producer = Effect.gen(function* (_) {
    for (let i = 0; i < 5; i++) {
      yield* _(Queue.offer(queue, `メッセージ-${i}`));
      yield* _(Effect.sleep("100 millis"));
    }
    yield* _(Queue.offer(queue, "DONE"));
  });

  const consumer = Effect.gen(function* (_) {
    while (true) {
      const msg = yield* _(Queue.take(queue));
      if (msg === "DONE") break;
      console.log(`消費: ${msg}`);
    }
  });

  yield* _(
    Effect.all([producer, consumer], { concurrency: 2 })
  );
});

// レート制限付き並列リクエスト
const fetchWithConcurrencyLimit = (urls: string[]) =>
  Effect.gen(function* (_) {
    const semaphore = yield* _(Effect.makeSemaphore(3));

    const results = yield* _(
      Effect.all(
        urls.map((url) =>
          semaphore.withPermits(1)(
            Effect.tryPromise({
              try: () => fetch(url),
              catch: (e) => new FetchError(e),
            })
          )
        ),
        { concurrency: "unbounded" }
      )
    );

    return results;
  });

// Fiber 監視
const supervisedExample = Effect.gen(function* (_) {
  const fiber = yield* _(
    Effect.fork(
      Effect.gen(function* (_) {
        yield* _(Effect.sleep("5 seconds"));
        return "完了";
      })
    )
  );

  yield* _(
    Fiber.interrupt(fiber).pipe(
      Effect.race(Fiber.join(fiber))
    )
  );
});

5 つのよくある落とし穴と解決策

1. Effect の実行忘れ

// ❌ 間違い:Effect は遅延評価、実行しないと動作しない
const program = Effect.succeed(42).pipe(Effect.map(console.log));

// ✅ 正しい:明示的に実行する必要がある
Effect.runPromise(program);

2. Effect 内での Promise の混用

// ❌ 間違い:async/await は Effect のエラートラッキングを破壊する
const bad = Effect.async(() => {
  const data = await fetch("/api"); // 構文エラー
});

// ✅ 正しい:Effect.tryPromise でラップ
const good = Effect.tryPromise({
  try: () => fetch("/api"),
  catch: (e) => new FetchError(e),
});

3. タグなしのエラー型

// ❌ 間違い:生の Error は catchTag を使えない
const bad = Effect.fail(new Error("something went wrong"));

// ✅ 正しい:_tag 付きカスタムエラーを使用
class AppError {
  readonly _tag = "AppError";
  constructor(readonly message: string) {}
}
const good = Effect.fail(new AppError("something went wrong"));

4. Layer の提供忘れ

// ❌ 間違い:Database を使用しているが Layer が提供されていない
const program = Effect.gen(function* (_) {
  const db = yield* _(Database);
  return yield* _(db.query("SELECT 1"));
});
Effect.runPromise(program); // 実行時エラー:Database の Layer が不足

// ✅ 正しい:Layer を提供する
Effect.runPromise(program.pipe(Effect.provide(PostgresLive)));

5. Fiber の中断を無視

// ❌ 間違い:中断不可能な Effect は Fiber のクリーンアップを妨げる
const bad = Effect.sync(() => {
  while (true) {} // 無限ループ、中断できない
});

// ✅ 正しい:Effect.yieldNow で制御を譲渡
const good = Effect.gen(function* (_) {
  while (true) {
    yield* _(Effect.yieldNow());
  }
});

10 のエラートラブルシューティング項目

# エラー現象 原因 解決策
1 Error: Service not found: Database Layer が提供されていない Effect.provide() で Layer を注入
2 Uncaught TypeError: Effect is not a function インポートパスの間違い import { Effect } from "effect" を確認
3 Effect が実行されない Effect.runPromise 等の呼び出し忘れ Effect は遅延評価、明示的な実行が必要
4 catchTag がエラーにマッチしない エラークラスに _tag プロパティがない カスタムエラーに readonly _tag を追加
5 型推論が失敗し never になる エラー型と catch が一致しない Effect のエラー型パラメータを確認
6 Fiber interrupted が予期せず発生 親 Fiber 終了による子 Fiber の中断 Effect.forkDaemon でデーモン Fiber を作成
7 メモリリーク Fiber が適切にクリーンアップされていない 構造化並行を使用、forkDaemon の乱用を避ける
8 Schedule リトライが動作しない リトライ条件を満たしていない whileInput / whileOutput 条件を確認
9 Layer の循環依存 2 つの Layer が相互依存 依存関係をリファクタリング、Layer.provide で分割
10 timeout が動作しない タイムアウト時間の単位エラー 数値ではなく文字列形式 "5 seconds" を使用

Effect-TS vs Promise/async-await 比較

次元 Effect-TS Promise/async-await
エラー型 コンパイル時に完全に型付け unknown、手動アサーションが必要
遅延評価 ✅ 遅延実行 ❌ 即時実行
合成可能性 ✅ pipe + gen ⚠️ 手動ラップが必要
依存性注入 ✅ Layer システム ❵ 組み込みソリューションなし
並行制御 ✅ Fiber + Semaphore ❌ ネイティブサポートなし
リトライ戦略 ✅ Schedule ❌ 手動実装
タイムアウト制御 ✅ 組み込み ❌ Promise.race ハック
リソース安全性 ✅ Scope + acquireRelease ⚠️ finally は保証されない
中断サポート ✅ Fiber.interrupt ❌ AbortController
テスト容易性 ✅ Layer を差し替えるだけ ⚠️ モジュールモックが必要
デバッグ体験 ⚠️ 深いスタックトレース ✅ クリアなネイティブスタック
バンドルサイズ ⚠️ ~80KB min ✅ 0KB(ネイティブ)

おすすめツール

Effect-TS 開発で生産性を向上させるオンラインツール:


まとめ:Effect-TS は副作用を値としてモデル化することで、TypeScript にかつてない型安全性と合成可能性をもたらします。Effect、Layer、Service、Fiber の 4 つのコアコンセプトをマスターし、catchTag、retry、timeout などのエラー処理パターンを組み合わせれば、2026 年に真に堅牢な関数型 TypeScript コードを書くことができます。学習曲線は急ですが、一度マスターすれば、生の Promise に戻ることは二度とありません。

ブラウザローカルツールを無料で試す →

#TypeScript Effect#Effect-TS#副作用处理#函数式编程#2026