FLINTERS BLOG
📖

Effect-Tsいいよ みんなも使おう Effect Effect

に公開

こんにちは、FLINTERSの石橋です。日頃Webアプリケーションを作っています。好きなPostgreSQL拡張はPostGISです。

弊社菅野がTypeScript界のZIOことEffectを布教しています(ブログ記事)。わたしも最近使い始めたのですが、とにかく便利でハイな気分になったので紹介記事です。

エラーハンドリングが強い

TypeScript(JavaScript)の鬼門ことエラーハンドリングですが、Effectは型パラメータでエラーが表現されます。

         ┌─── Represents the success type
         │        ┌─── Represents the error type
         │        │      ┌─── Represents required dependencies
         ▼        ▼      ▼
Effect<Success, Error, Requirements>

// https://effect.website/docs/getting-started/the-effect-type/

何が起こるかわからないプレーンでナイーブな処理もEffect.try(あるいはEffect.tryPromiseなど)で包めばもう安心です。

playground

import { Effect, Data } from "effect";

// Tag付きエラー型を定義
class KnownError extends Data.TaggedError("KnownError")<{ message: string }> {}
class UnknownError extends Data.TaggedError("UnknownError")<{
  message: string;
  cause: unknown;
}> {}

// task: Effect.Effect<number, KnownError | UnknownError, never> と型推論される
// Effect.Effectの型パラメータは1. 正常系戻り値, 2. エラー, 3. 依存
const task =
  Effect.try({
    try: () => {
      throw new Error("known error occured");
      return 1;
    },
    catch: (e) =>
      e instanceof Error
        ? new KnownError({ message: e.message })
        : new UnknownError({ message: String(e), cause: e }),
  });

// tap: 正常系副作用, tapError: 異常系副作用, catchTags: 特定エラーをcatch
const program = task.pipe(
  Effect.tap(Effect.logInfo),
  Effect.tapError((e) => Effect.logError(e)), // eは KnownError | UnknownErrorと解決される
  Effect.catchTags({ KnownError: () => Effect.succeed(0) }), // KnownErrorは握りつぶす
);

Effect.runSync(program);

// timestamp=2025-08-26T23:24:09.379Z level=ERROR fiber=#0 message="{
//   \"message\": \"known error occured\",
//   \"_tag\": \"KnownError\"
// }"

制御フロー

Effect.genで作成するGenerator内ではJavaScriptの制御構文が使えるので取っつきやすいです。

playground

// Admin専用処理を行う例
import { Effect } from "effect";

type Admin = {
  email: string;
  _tag: "admin";
};

type StandardUser = {
  email: string;
  _tag: "standardUser";
};

type User = Admin | StandardUser;

const resolveUser: Effect.Effect<User, never, never> = Effect.succeed({
  email: "hoge@invalid.com",
  _tag: "admin",
});

const adminTask = Effect.void;

const program = Effect.gen(function* () {
  const user: User = yield* resolveUser;
  switch (user._tag) {
    case "admin":
      yield* Effect.logInfo("start admin task");
      yield* adminTask;
      yield* Effect.logInfo("finish admin task");
      break;
    case "standardUser":
      yield* Effect.logInfo("unauthorized user");
      break;
    default:
      const exhaustivenessCheck: never = user;
      break;
  }
});

Effect.runSync(program);

// timestamp=2025-08-26T23:26:53.284Z level=INFO fiber=#0 message="start admin task"
// timestamp=2025-08-26T23:26:53.287Z level=INFO fiber=#0 message="finish admin task"

複雑な制御をしたい時はEffect組み込み関数を使いましょう。
とにかく種類が豊富なので大抵のユースケースは満たせます。
並列処理(上限付き)も簡単に書けます。

playground

import { Effect, pipe } from "effect";

const job = (id: number, delay: number) =>
  Effect.gen(function* () {
    yield* Effect.logInfo(`start ${id}`);
    yield* Effect.sleep(delay);
    yield* Effect.logInfo(`done ${id}`);
  });

const program = Effect.all([job(1, 1000), job(2, 2000), job(3, 3000)], {
  concurrency: 2, // 無制限で回したいときは"unbounded"
});

Effect.runPromise(program);

// timestamp=2025-08-27T01:50:09.729Z level=INFO fiber=#2 message="start 1"
// timestamp=2025-08-27T01:50:09.730Z level=INFO fiber=#3 message="start 2"
// timestamp=2025-08-27T01:50:10.736Z level=INFO fiber=#2 message="done 1"
// timestamp=2025-08-27T01:50:10.738Z level=INFO fiber=#2 message="start 3"
// timestamp=2025-08-27T01:50:11.736Z level=INFO fiber=#3 message="done 2"
// timestamp=2025-08-27T01:50:13.745Z level=INFO fiber=#2 message="done 3"

リトライ

Scheduleを使って宣言的に書けるのがいいですね。

playground

import { Effect, Schedule } from "effect";

// 常に失敗する疑似API
const fetch: Effect.Effect<string, Error, never> = Effect.fail(
  new Error("error occured"),
);

const schedule = Schedule.intersect(
  Schedule.exponential("10 millis"),
  Schedule.recurs(5),
);

const program = Effect.gen(function* () {
  const response = yield* fetch.pipe(
    Effect.tapError((err) => Effect.logError(`fetch failed: ${String(err)}`)),
    Effect.retry(schedule),
  );
  yield* Effect.logInfo(response);
});

Effect.runPromise(program);
// timestamp=2025-08-27T00:09:41.566Z level=ERROR fiber=#0 message="fetch failed: Error: error occured"
// timestamp=2025-08-27T00:09:41.583Z level=ERROR fiber=#0 message="fetch failed: Error: error occured"
// timestamp=2025-08-27T00:09:41.606Z level=ERROR fiber=#0 message="fetch failed: Error: error occured"
// timestamp=2025-08-27T00:09:41.654Z level=ERROR fiber=#0 message="fetch failed: Error: error occured"
// timestamp=2025-08-27T00:09:41.749Z level=ERROR fiber=#0 message="fetch failed: Error: error occured"
// timestamp=2025-08-27T00:09:41.918Z level=ERROR fiber=#0 message="fetch failed: Error: error occured"
// (FiberFailure) Error: error occured
//     at Object.eval (/ho(略...

ScalaのZIOで見かけるポーリング処理を同じように実装できます。

playground

import { Effect, Schedule, Data } from "effect";

type Response = {
  code: number;
  data: Record<string, string>[];
};

class AcceptedButNotFinishedError extends Data.TaggedError(
  "AcceptedButNotFinishedError",
)<{}> {}
class FetchError extends Data.TaggedError("FetchError")<{}> {}

// 処理中ステータスしか返さない疑似API
const fetch: Effect.Effect<Response, FetchError, never> = Effect.logInfo(
  "try fetch",
).pipe(
  Effect.as({
    code: 202,
    data: [],
  }),
);
// エラーを出す疑似API
// const fetch: Effect.Effect<Response, FetchError, never> = Effect.fail(new FetchError())

// AcceptedButNotFinishedErrorの場合だけリトライするScheduler
const schedule = Schedule.spaced("2 seconds").pipe(
  Schedule.intersect(Schedule.recurUpTo("10 seconds")),
  Schedule.intersect(
    Schedule.recurWhile((err) => err instanceof AcceptedButNotFinishedError),
  ),
);

const program = Effect.gen(function* () {
  // ポーリング実施
  const response = yield* fetch.pipe(
    Effect.flatMap((res) =>
      res.code === 202
        ? Effect.fail(new AcceptedButNotFinishedError())
        : Effect.succeed(res),
    ),
    Effect.retry(schedule),
  );
  yield* Effect.logInfo(response);
});

Effect.runPromise(program);

// timestamp=2025-08-27T00:41:54.325Z level=INFO fiber=#0 message="try fetch"\
// timestamp=2025-08-27T00:41:56.338Z level=INFO fiber=#0 message="try fetch"\
// timestamp=2025-08-27T00:41:58.348Z level=INFO fiber=#0 message="try fetch"\
// timestamp=2025-08-27T00:42:00.358Z level=INFO fiber=#0 message="try fetch"\
// timestamp=2025-08-27T00:42:02.374Z level=INFO fiber=#0 message="try fetch"\
// timestamp=2025-08-27T00:42:04.382Z level=INFO fiber=#0 message="try fetch"\
// (FiberFailure) AcceptedButNotFinishedError: An error has occurred\
//     at new AcceptedButNotFinishedError (以下略

Dependency Injection(DI)

DIもできます。Effect.Effectの3つ目の型パラメータは依存関係を表します。

         ┌─── Represents the success type
         │        ┌─── Represents the error type
         │        │      ┌─── Represents required dependencies
         ▼        ▼      ▼
Effect<Success, Error, Requirements>

// https://effect.website/docs/getting-started/the-effect-type/
// https://effect.website/docs/requirements-management/services/

下の例ではUserRepositoryをDIしています。repositoryLayerを作っていますが、Effectでは複数の依存を一つのLayerにどんどんマージしたり依存ツリーを構築してまとめてDIしたりもできるので利用側の実装が楽です。
また、依存関係が満たされないEffectを実行しようとするとTypeScriptの型チェックでエラーが出ます。

playground

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

type User = {
  id: string;
  email: string;
};

// UserRepositoryのinterface
class UserRepository extends Context.Tag("UserRepository")<
  UserRepository,
  {
    readonly resolveByEmail: (
      email: string,
    ) => Effect.Effect<Option.Option<User>, unknown, never>;
    readonly store: (user: User) => Effect.Effect<void, unknown, never>;
  }
>() {}

// UserRepositoryを使うユースケース
const usecase: Effect.Effect<void, unknown, UserRepository> = Effect.gen(
  function* () {
    const repository = yield* UserRepository;
    const user = yield* repository.resolveByEmail("hoge@invalid.com");
    yield* Effect.logInfo(user);
  },
);

// UserRepositoryの実装
const repositoryLayer = Layer.succeed(UserRepository, {
  resolveByEmail: (email: string) =>
    Effect.succeed(Option.some({ id: "hogehoge", email })),
  store: (_: User) => Effect.void,
});

// DIしてユースケースを実行
Effect.runPromise(Effect.provide(usecase, repositoryLayer));
// Effect.runPromise(usecase); // UserRepositoryをDIしないと型エラー

// timestamp=2025-08-27T02:26:41.717Z level=INFO fiber=#0 message="{
//   \"_id\": \"Option\",
//   \"_tag\": \"Some\",
//   \"value\": {
//     \"id\": \"hogehoge\",
//     \"email\": \"hoge@invalid.com\"
//   }
// }"

llm.txtもあります

Effect機能多すぎて何をどう使ったらいいかわからない!
pipeで型エラー地獄に嵌ってしまった!
そんな時はAI Agentにllm.txtを読ませて、困ったときに頼りになるEffectマスターを錬成できます。

https://effect.website/docs/getting-started/introduction/#docs-for-llms

紹介は以上です。
TypeScriptでこんな素敵なものが作れるんですね驚きですね。
一度使うと二度とタダのTypeScriptには戻れない、とにかく多機能で便利なEffectの魅力が少しでも伝われば幸いです。
みんなもEffectで幸せになりましょう

FLINTERS BLOG
FLINTERS BLOG

Discussion