Try-Catchの不確実性からの脱却
ほとんどのTypeScript開発チームは、標準的なasync/awaitでいずれ壁に突き当たります。コードをtry/catch ブロックで囲みますが、JavaScriptのエラーはany型として扱われるため、何かがうまくいかなくなった瞬間に型安全性がすべて失われてしまいます。失敗が許されないエンタープライズ環境において、catchブロックがあらゆるエッジケースを処理してくれると「期待する」ことは、戦略ではなく負債です。
Effect TSを単なるライブラリではなく、TypeScriptに「欠けているピース」を補う標準ライブラリとして考えてみてください。Effectはすべてのアクションを「値」として扱います。つまり、コアロジックを変更することなく、あらゆる操作を検査、計測、再試行、またはキャンセルできるということです。私の経験では、複雑なフィンテックのバックエンドをEffectに移行した際、最初の2ヶ月でハンドルされていない実行時例外が60%減少しました。
クイックスタート:最初のEffectを実行する(5分)
抽象的な理論は抜きにして、コードを動かしてみましょう。Effectは、いくつかのカテゴリーのミドルウェアを置き換える単一のパッケージです。お好みのパッケージマネージャーでインストールしてください。
npm install effect
Effectは本質的に「設計図」です。何が起こるべきかを記述しますが、明示的に指示するまで実行されません。この定義と実行の分離こそが、コードの予測可能性を高める要因です。以下は、基本的な同期Effect ofの作成方法です。
import { Effect } from "effect";
// 1. 設計図を定義する
const program = Effect.sync(() => {
console.log("Effectが実行中!");
return 42;
});
// 2. プログラムを実行する
const result = Effect.runSync(program);
console.log(result); // 出力: 42
実際のアプリケーションでは、主に非同期タスクを扱うことになります。EffectはrunPromiseを使用してこれをシームレスに処理し、既存のコードベースと共存させることができます。
const asyncProgram = Effect.promise(() =>
Promise.resolve("APIデータを受信しました")
);
Effect.runPromise(asyncProgram).then(console.log);
「3つの穴」モデル:Effect<Success, Error, Requirements>
このエコシステムの魔法は、Effect<A, E, R>という型にあります。この3つのパラメータをマスターすれば、このライブラリを使いこなせます。関数のあらゆる側面を明示することを強制するため、よく「3つの穴(Three-Hole)」モデルと呼ばれます。
- A (Success): どのような値を返すか?(例:
Userやstring) - E (Error): 具体的にどのようなエラーが発生しうるか?
Promise<any>とは異なり、特定のエラー型がリストアップされます。 - R (Requirements): 実行に何が必要か?これはデータベース接続やAPIキーなどが該当します。
ユーザーを取得する関数を考えてみましょう。標準的なTypeScriptでは、シグネチャがリスクを隠してしまいます。Effectでは、シグネチャがすべてを物語ります。
interface User { id: number; name: string; }
class DatabaseError { readonly _tag = "DatabaseError"; }
// シグネチャが示す内容:Userを返し、DatabaseErrorで失敗する可能性があり、コンテキスト(環境)は不要
const getUser = (id: number): Effect.Effect<User, DatabaseError, never> => {
return Effect.succeed({ id, name: "Jane Doe" });
};
// この透明性はデバッグにおいて大きな利点となります。サービスが失敗したとき、IDEはハンドルし忘れているエラー型を正確に教えてくれます。
デフォルトで備わっているレジリエンス:再試行とロジック
一時的なネットワークの不具合は、エンタープライズソフトウェアの天敵です。通常、再試行ロジックを追加するには、煩雑なループやサードパーティのラッパーが必要になります。Effectはレジリエンスを第一級市民として扱います。
例えば、不安定なサードパーティAPIがあるとします。それをラップし、高度な再試行ポリシーをわずか一行のコードで適用できます。
import { Schedule } from "effect";
const flakyCall = Effect.tryPromise({
try: () => fetch("https://api.stripe.com/v1/charges").then(res => res.json()),
catch: () => new Error("ネットワークタイムアウト")
});
// 3回再試行し、各試行の間に1秒待機する
const resilientCall = Effect.retry(
flakyCall,
Schedule.spaced("1 second").pipe(Schedule.recurs(3))
);
よりクリーンで、格段に安全です。エラーが型付けされているため、きめ細かな制御が可能です。例えば、503 Service Unavailableの場合は再試行し、401 Unauthorizedの場合はリソースを節約するために即座に失敗させるといったことが可能です。
肥大化させない依存性の注入(DI)
Rパラメータは、重いフレームワークやデコレータを必要とせずに依存性の注入(DI)を処理します。サービスに「タグ(Tag)」を定義し、それらを満たす「レイヤー(Layer)」を提供します。
import { Context, Layer } from "effect";
class ConfigService extends Context.Tag("ConfigService")<
ConfigService,
{ readonly getUrl: () => Effect.Effect<string> }
>() {}
const ConfigLive = Layer.succeed(
ConfigService,
{ getUrl: () => Effect.succeed("https://production-api.com") }
);
const program = ConfigService.pipe(
Effect.flatMap((service) => service.getUrl())
);
このアプローチにより、テストが非常に容易になります。ビジネスロジックに手を加えることなく、VitestスイートでConfigLiveをConfigTestレイヤーに差し替えることができます。グローバルモジュールを手動でモックする必要はもうありません。
本番環境に導入するための実践的ヒント
メリットを実感するために、一晩でアーキテクチャ全体を書き換える必要はありません。既存のチームにEffectを導入するための推奨ステップは以下の通りです。
- 「末端」の関数をターゲットにする: 最も不安定なAPIコールや複雑なファイルシステム操作をEffectでラップすることから始めましょう。
- Pipeを活用する:
.pipe()メソッドを使用してください。深くネストされた関数呼び出しを、上から下へと流れる読みやすい一連のステップに変換してくれます。 - エラーにタグを付ける: エラーに単純な文字列を使用するのは避けましょう。
_tagフィールドを持つオブジェクトを使用することで、Effect.catchTagを使って特定の失敗を処理し、それ以外を上位に伝播させることができます。 - エコシステムを利用する: データバリデーションが必要なら
@effect/schemaを、HTTPルーティングが必要なら@effect/platformを検討してください。これらは同じ基礎ロジックを共有しています。
学習曲線は確かにありますが、それに見合う価値はすぐに得られます。数週間もすれば、バグの追跡が容易になり、型定義が実際に本番環境の障害からあなたを守り始めるでしょう。
まとめ
Effect TSは、Web開発に固有の混乱に構造をもたらします。エラーと依存関係を明示的にすることで、「防御的なコーディング」を型システムの組み込み機能へと変えてくれます。ミッションクリティカルなTypeScriptアプリを構築するすべての人にとって、Effectをマスターすることは、単なる「スクリプト作成」から「堅牢なシステムのエンジニアリング」へと移行するための最短ルートです。

