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
など)で包めばもう安心です。
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の制御構文が使えるので取っつきやすいです。
// 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組み込み関数を使いましょう。
とにかく種類が豊富なので大抵のユースケースは満たせます。
並列処理(上限付き)も簡単に書けます。
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
を使って宣言的に書けるのがいいですね。
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で見かけるポーリング処理を同じように実装できます。
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の型チェックでエラーが出ます。
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マスターを錬成できます。
紹介は以上です。
TypeScriptでこんな素敵なものが作れるんですね驚きですね。
一度使うと二度とタダのTypeScriptには戻れない、とにかく多機能で便利なEffectの魅力が少しでも伝われば幸いです。
みんなもEffectで幸せになりましょう
Discussion