try/catch で囲む箇所が増えるたびに、どのエラーが投げられるのか自信が持てなくなります。Node.js で TypeScript を書いている方なら、一度はこの感覚に出会ったのではないでしょうか。Effect-TS(@effect/io)は、その不安を型システムに肩代わりさせるためのライブラリです。
ただ、Effect-TS はジェネレータ構文と高機能な型推論を駆使するため、補完がほとんど効かないエディタで書こうとすると一気に厳しくなります。Antigravity の AI 補完は、Effect の R・E・A パラメータを文脈から読み取ってくれるため、このライブラリと組み合わせると体験が一段変わります。
なぜ Antigravity は Effect-TS と相性がいいのか
Effect-TS のコアは Effect<Requirements, Error, Success> という三型パラメータの計算型です。async/await と違い、エラー型まで型に持ち上がるため、関数のシグネチャだけで「何が失敗しうるか」が読み取れます。
ただし、この恩恵を実装側で受け取るには、ジェネレータ内でエラーを正しく yield* _(...) する規律が必要です。Antigravity の AI 補完は、上記の三型パラメータを推論しつつ「次にどんなエラーが起こりうるか」を提示してくれるので、書きながら型を整えるサイクルが回しやすくなります。
私はこのとき、Antigravity のサイドバーに node_modules/@effect/io/dist/esm/index.d.ts を開きっぱなしにしておくと、AI が型情報を強く参照してくれることに気づきました。型定義ファイルを文脈に含めること、これが Effect-TS を Antigravity で書くときの最初のコツです。
TypeScript の strict 設定を Antigravity で運用するガイド と合わせると、補完精度がさらに伸びます。
最初の Effect.gen ブロックを書く
まずは、HTTP リクエストとデータベースアクセスを組み合わせる最小例です。Antigravity に「Effect.gen で書き直して」と指示すると、以下のような構造が返ってきます。
import { Effect } from "effect"
import { HttpClient } from "@effect/platform"
// fetchUserProfile: ユーザーIDを受け取り、API から取得して整形する
// 戻り型: Effect<HttpClient, FetchError | ParseError, UserProfile>
const fetchUserProfile = (userId: string) =>
Effect.gen(function* (_) {
const client = yield* _(HttpClient.HttpClient)
const response = yield* _(
client.get(`/api/users/${userId}`)
)
const json = yield* _(response.json)
return parseUserProfile(json) // Schema でバリデート
})
// 期待される実行結果(Layer 提供後):
// → 成功時: UserProfile オブジェクト
// → 失敗時: FetchError または ParseError が型で伝播ここで重要なのは、Effect.gen の中ではエラーが投げられない点です。yield* _(...) で受け取った値は成功時の値だけで、エラーは型パラメータ E 側に積み上がります。fetchUserProfile を呼び出す側は、HttpClient を Layer で提供しないとコンパイルが通らないため、依存関係も型で表現される設計です。
ドメインエラーを共用体型で設計する
Effect-TS で陥りやすい罠は、エラーをすべて Error クラスでまとめてしまうパターンです。これでは型システムの恩恵が半減します。私は、ドメインごとに Data.TaggedError を使った判別共用体を作るやり方をおすすめします。
import { Data } from "effect"
// 各エラーは _tag フィールドで判別可能な共用体になる
class UserNotFound extends Data.TaggedError("UserNotFound")<{
userId: string
}> {}
class RateLimitExceeded extends Data.TaggedError("RateLimitExceeded")<{
retryAfter: number
}> {}
class ParseError extends Data.TaggedError("ParseError")<{
reason: string
}> {}
type FetchUserError = UserNotFound | RateLimitExceeded | ParseError
// 期待動作: catchTag で個別ハンドリング後、E パラメータからその型が消えるこの設計なら、Effect.catchTag("RateLimitExceeded", err => ...) のように特定エラーだけを受けて再試行する処理が、すべて型で守られます。catchTag で捕まえた後の Effect から、捕捉したエラー型が型レベルで消えるのも気持ちが良いところです。
AI エージェントの本番エラーハンドリング設計 でも触れたように、エラーは「種類で分類して、それぞれに専用の回復戦略を持つ」ことで運用が安定します。
Layer による依存注入で本番とテストを切り分ける
Effect-TS の Layer は、依存関係を構築する手順を値として表現する仕組みです。本番ではリアルな HTTP クライアントを、テストでは固定値を返すスタブを供給する、といった切替が型を壊さずにできます。
import { Layer, Effect } from "effect"
import { HttpClient } from "@effect/platform"
// 本番用: 実 HTTP クライアントを構築
const HttpClientLive = HttpClient.layer
// テスト用: 任意のレスポンスを返すスタブ
const HttpClientTest = Layer.succeed(
HttpClient.HttpClient,
HttpClient.makeWith({
request: () => Effect.succeed(mockResponse),
})
)
// 実行時に Layer を差し替えるだけで切替可能
const program = fetchUserProfile("u_123")
// 本番:
Effect.runPromise(program.pipe(Effect.provide(HttpClientLive)))
// → 実 API にリクエストし、UserProfile が返る
// テスト:
Effect.runPromise(program.pipe(Effect.provide(HttpClientTest)))
// → スタブから固定値が返り、外部依存なしで検証可能Antigravity に「この program 用のテスト Layer を生成して」と頼むと、上記のスタブパターンを高い精度で書いてくれます。Layer の合成(Layer.merge や Layer.provideMerge)も、ライブラリの型定義をエディタが見ているおかげで補完が安定します。
Antigravity で Effect ジェネレータを書くコツ
実際に書き続けて気づいた、補完精度を上げる工夫を3つご紹介します。
第一に、ファイルの先頭で必要な import を全部書ききることです。Effect、Data、Layer などはモジュールパスが似ているため、後から AI に補完させると遠回りすることがあります。先に枠を作っておくと、関数本体の補完が早くなります。
第二に、戻り型コメントを関数定義の直前に書くことです。「// 戻り型: Effect<R, E, A>」という1行があるだけで、Antigravity の AI が Effect.gen 内のジェネレータをかなり精緻に補完してくれます。
第三に、yield* _(...) か新構文(_ を省略するスタイル)かをプロジェクト内で統一することです。混在していると AI が両方を提案して補完が乱れます。tsconfig の target を ES2022 以上に上げ、どちらの構文を採用するかチームで決めておきましょう。
このあたりの周辺知識は、Zod を中心としたスキーマ駆動開発の記事 で扱った原則とも共通しています。型を最初に固める、というワークフロー設計は、Effect-TS でも Zod でも変わりません。
今日から始める一歩
新しいプロジェクトでいきなり Effect-TS を全面採用するのは、私の経験では負荷が大きすぎます。おすすめは、エラーハンドリングが特に複雑な1関数を選び、そこだけ Effect で書き直すことです。Antigravity に既存の関数を貼り付けて「Effect.gen で書き直し、エラーを TaggedError で表現してください」と頼むと、十分な雛形が生成されます。
そこから1日1関数のペースで広げていくと、2週間ほどで主要な API ハンドラがすべて型で守られた状態になります。手元のプロジェクトで「最も try/catch がネストしている関数」を1つ選ぶところから始めてみてはいかがでしょうか。
書籍で体系的に