Promise型には本番環境における死角がある
コードベースにおける一般的な契約である Promise<User> を想像してみてください。この型はハッピーパスを完璧に記述しており、すべてがうまくいけば User オブジェクトが返されることを期待します。しかし、現実世界はどうでしょうか?この型は、HTTPエラー、ユーザーが見つからない場合、あるいはリクエストが無限にハングアップしてアプリケーションが待機し続けるような状況については何も語っていません。
残念ながら、TypeScriptはPromiseの拒否(rejection)を確実には型付けできません。catch ハンドラーは多くの場合 unknown 値を受け取るため、実行時に何が問題だったのかを推測せざるを得ません。つまり、チームは堅牢なコンパイラチェックではなく、試行錯誤を通じて障害時の動作を確立するために貴重な時間を費やすことになります。
ユーザープロフィールを取得するサービスを考えてみましょう。呼び出し側には、単なる User や型のないエラー以上の情報が必要です。一時的なネットワークの不具合のように自動的にリクエストを再試行すべきか、明確な「ユーザーが見つかりません」というメッセージを表示すべきか、あるいは特定のタイムアウト後に待機を停止すべきかを知る必要があるのです。型にこのような明確さがなければ、本番環境は死角となり、なぜ障害が発生したのかという重要な情報が隠されてしまいます。
Effectは契約に欠けている要素を追加する
Effectは契約を Promise<User> からより堅牢なものへと変えます。そのコアとなるシグネチャ Effect<A, E, R> は、あらゆる計算において重要な3つの側面、つまり成功値、型付けされた失敗、そして必要な依存関係を正確に記述します。
詳しく見ていきましょう。A は成功時に得られる値(例の User など)を表します。E は失敗時の魔法が起こる場所であり、HttpError | NotFound のように、予想されるすべてのエラーを 型付けされたユニオン として提供します。最後に R は「要件(requirements)」を表し、HTTPクライアントやデータベース接続など、コードの実行に必要なサービスや依存関係を指します。
動画で紹介したユーザー検索関数を考えてみましょう。単なる Promise<User> ではなく、そのEffect型は Effect<User, HttpError | NotFound, SomeHttpClient> となる可能性があります。これにより契約が明示的になります。つまり、User が得られる可能性があるだけでなく、HttpError や NotFound という条件が具体的かつ予想される失敗モードであることを教えてくれるのです。
これは通常のPromiseとは大きな違いです。Effectの計算は 遅延評価される値(lazy values) であり、何をするか は記述しますが、いつするか は記述しません。再試行、タイムアウト、エラー処理ポリシーを含む計算全体を、実行の 前 に構成します。これにより、プログラムを構築する際にすべての要件と潜在的な結果が可視化され、死角が詳細な地図へと変わります。
安全性を高めるにつれて変化するエラー型を確認する
データ取得パイプラインに回復力を追加する際、エラー型がどのように進化するかを追跡してみましょう。最初は、Effect<User, HttpError | NotFound, R> は HttpError または NotFound エラーで失敗する可能性があります。
まず、一時的な HttpError を処理するために指数バックオフの再試行ポリシーを適用しますが、再試行によって HttpError の 可能性 が排除されるわけではないため、型シグネチャは変わりません。次に、2秒のタイムアウトを追加します。これにより、TimeoutError がエラー型に導入され、Effect<User, HttpError | NotFound | TimeoutError, R> となります。
その後、フォールバック値を提供することで NotFound ケースを明示的に処理します。魔法のように、NotFound 型はエラーシグネチャから消滅します。なぜなら、それが処理されることが保証されたからです。型は Effect<User, HttpError | TimeoutError, R> になります。これは単なる巧妙なトリックではなく、コンパイラが潜在的な失敗をリアルタイムで管理しているのです。
これを証明するために、タイムアウトをわずか50ミリ秒に短縮します。コードを実行すると、型システムが予測した通りに TimeoutError が発生します。ランタイムで検証されるこのコンパイル時のフィードバックループは、Effect: Production-Grade TypeScript の強力な機能であり、障害を可視化して管理可能にする同ライブラリのアプローチを象徴しています。
この記事が気に入ったら、毎朝同じようなものをメールで受け取れます。
1日1通 · 2クリックで解除 · サードパーティのトラッキングなし
得られる成果と移行のコスト
明示的なエラー追跡と依存関係の管理は、認証や決済といったクリティカルなサービスにおいて真価を発揮します。これらの領域では、外部APIのタイムアウトやデータベース接続の切断といった隠れた拒否パス(rejection paths)が、復旧やレビューを非常に困難にします。Effectの型駆動アプローチにより、こうした潜在的な障害モードに対して、事後対応ではなく先回りして対処することが可能になります。
Effectは単なる Result 型以上のものを提供します。リトライ、タイムアウト、並行処理、依存関係管理のための強力なランタイムツールが標準で備わっています。既存の Promise ベースのコードを即座に置き換えるのではなく、Effectはそれを補完し、アプリケーションの特定の高価値な部分から段階的に導入・統合していくことができます。
Effectの導入には学習コストが必要です。Effect<A, E, R> という独特のシグネチャを持つモデル、特にパイプラインを通じて エラー型 がどのように変化していくかを理解するには時間がかかります。Effectの型はアプリケーションの境界を越えて自然に伝播するため、チームは、特に機密性の高いシステムにおいて、信頼性と可観測性の向上というメリットと、学習および移行コストを慎重に比較検討する必要があります。
よくある質問
TypeScriptにおけるEffectとは何ですか?
Effectは、明示的な成功値、型付けされたエラー、および必要なサービスを用いて非同期プログラムをモデル化するためのライブラリです。
EffectはPromiseとどう違うのですか?
Promiseは解決または拒否される可能性のある値を記述しますが、拒否の型をエンコードしません。Effectは、型付けされた障害と依存関係もモデル化します。
エラーが処理されると、Effectの型はどのように変化しますか?
型付けされたエラーを処理すると、その障害はEffectのエラー型から削除されます。一方、タイムアウトのような操作を追加すると、新しい型付けされた障害が追加される可能性があります。
すべてのTypeScriptプロジェクトでEffectを採用すべきですか?
必ずしもそうではありません。明示的な障害処理やランタイム制御が必要な複雑なシステムには役立ちますが、その概念や型のフットプリントを理解するための導入コストが必要です。

