effect-ts について
Effect-TSを使うTypeScript開発者向けに、型付きeffect、並行処理、リソース管理、APIの基本パターンを解説するガイドです。MCPサーバーのリクエスト処理例や、AI生成コードで起こりやすい誤りも扱います。
主な機能
- 01型付きエラーと欠陥の処理
- 02Fiberによる並行処理と中断
- 03Layerによる依存性注入
- 04スコープを使ったリソース管理
- 05キャッシュ、再試行、スケジュール
- 06MCPリクエスト処理の例
活用シーン
- 01型付きエラー処理を備えたプログラムの設計
- 02並行タスクとそのライフサイクルの管理
- 03接続やファイルの確実な解放
- 04再試行、キャッシュ、可観測性の実装
- 05MCPサーバーのリクエストスコープ構成
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ kastalien-research/ thoughtbox-dot-claude --skill effect-tsインストール後、Claude Code で/effect-tsを入力して読み込まれたか確認します。
詳細
Effect-TS エキスパートガイド
Effect-TS は、型付きエフェクト、構造化並行処理、堅牢なランタイムを提供する関数型 TypeScript ライブラリです。このスキルでは、正しい使用パターンを解説し、LLM が生成したコンテンツによく見られる誤解に対処します。
クイックリファレンス
import { Effect, Layer, Context, Fiber, Schedule, Cache, Scope } from "effect";
import { Schema, JSONSchema } from "@effect/schema";
コアとなる型シグネチャ:
Effect<Success, Error, Requirements>
// ↑ ↑ ↑
// | | └── Dependencies (provided via Layers)
// | └── Expected errors (typed, must be handled)
// └── Success value
よくある誤解
LLM の出力には、誤った API が含まれていることがよくあります。 この表を使って修正してください。
| 誤り(AI の出力でよく見られるもの) | 正しいもの |
|---|---|
Effect.cachedWithTTL(...) | Cache.make({ timeToLive: Duration }) |
Effect.cachedInvalidateWithTTL(...) | cache.invalidate(key) / cache.invalidateAll() |
Effect.match(...) | Effect.either + Either.match、または Effect.catchTag |
| 「スレッドローカルストレージ」 | FiberRef による「ファイバーローカルストレージ」 |
| JSON Schema Draft 2020-12 | @effect/schema が生成するのは Draft-07 |
| ファイバーが「キャンセル」される | ファイバーは「終了」または「中断」される |
| すべてのキューにバックプレッシャーがある | バックプレッシャーがあるのは 有界 キューのみ。スライディングキューやドロッピングキューにはありません |
--only=production | --omit=dev(npm 7 以降) |
エラーハンドリング:2 種類のエラー
Effect は次の 2 種類のエラーを区別します。
- 想定されるエラー(失敗) -
Eチャネルで型付けされ、処理する必要があります - 想定外のエラー(欠陥) - ランタイムのバグ。捕捉されますが、型付けされません
// Expected error - typed
const fetchUser = (id: string): Effect.Effect<User, UserNotFoundError | NetworkError> => ...
// Handle expected errors
const handled = pipe(
fetchUser("123"),
Effect.catchTag("UserNotFoundError", (e) => Effect.succeed(defaultUser)),
Effect.catchTag("NetworkError", (e) => Effect.retry(schedule))
);
// Unexpected errors (defects) - captured by runtime
Effect.catchAllDefect(program, (defect) =>
Console.error("Unexpected error", defect)
);
ファイバーとキャンセル(MCP で重要)
ファイバーは、ネイティブな割り込み機能を備えた軽量な仮想スレッドです。
// Fork a fiber
const fiber = yield* Effect.fork(longRunningTask);
// Interrupt it (e.g., when MCP client disconnects)
yield* Fiber.interrupt(fiber);
// Structured concurrency: child fibers auto-terminate with parent
const parent = Effect.gen(function* () {
yield* Effect.fork(backgroundTask); // Auto-interrupted when parent ends
yield* mainTask;
});
// Daemon fibers outlive their parent
yield* Effect.forkDaemon(longLivedBackgroundTask);
並行処理プリミティブ
Effect.race - 最初に完了したものが勝ち、残りは中断されます
// First to succeed wins; other is automatically interrupted
const result = yield* Effect.race(
fetchFromCache,
fetchFromDatabase
);
並行数を制御する Effect.all
// Process 50 documents with max 5 concurrent
const results = yield* Effect.all(documents.map(processDoc), {
concurrency: 5 // NOT a "worker pool" - limits concurrent tasks
});
キューの種類
// Bounded - applies back-pressure (offer suspends when full)
const bounded = yield* Queue.bounded<string>(100);
// Dropping - discards new items when full (no back-pressure)
const dropping = yield* Queue.dropping<string>(100);
// Sliding - discards oldest items when full (no back-pressure)
const sliding = yield* Queue.sliding<string>(100);
依存性注入のための Layer
Layer は、依存関係を外部に漏らすことなくサービスを構築します。
// Define a service
class Database extends Context.Tag("Database")<
Database,
{ query: (sql: string) => Effect.Effect<Result> }
>() {}
// Create layer (dependencies handled at construction)
const DatabaseLive = Layer.effect(
Database,
Effect.gen(function* () {
const config = yield* Config; // Dependency injected here
return {
query: (sql) => Effect.tryPromise(() => runQuery(sql, config))
};
})
);
// Provide to program
const runnable = program.pipe(Effect.provide(DatabaseLive));
// For testing - swap implementation
const DatabaseTest = Layer.succeed(Database, {
query: () => Effect.succeed(mockResult)
});
リソース管理
Effect.ensuring - ファイナライザーを必ず実行
const program = pipe(
Effect.tryPromise(() => openConnection()),
Effect.ensuring(Console.log("Cleanup")) // Runs on success, failure, OR interrupt
);
acquireUseRelease パターン
const withConnection = Effect.acquireUseRelease(
Effect.tryPromise(() => db.connect()), // Acquire
(conn) => Effect.tryPromise(() => conn.query("SELECT *")), // Use
(conn) => Effect.promise(() => conn.close()) // Release (always runs)
);
リソースのライフサイクルを管理する Scope
Effect.scoped(
Effect.gen(function* () {
const file = yield* openFile("data.txt"); // Acquired
const data = yield* file.read();
return data;
}) // File automatically released when scope closes
);
キャッシュ
Effect.cachedWithTTL はありません。 Cache モジュールを使用してください。
import { Cache } from "effect";
const cache = yield* Cache.make({
capacity: 100,
timeToLive: Duration.minutes(5),
lookup: (key: string) => fetchExpensiveData(key)
});
// Use the cache
const value = yield* cache.get("my-key");
// Invalidate
yield* cache.invalidate("my-key");
yield* cache.invalidateAll();
Schedule による再試行
import { Schedule } from "effect";
// Retry 3 times with exponential backoff
const policy = Schedule.exponential("100 millis").pipe(
Schedule.intersect(Schedule.recurs(3))
);
const robust = Effect.retry(unstableOperation, policy);
// Retry until condition
const untilSuccess = Effect.retry(operation, {
until: (err) => err.code === "RATE_LIMITED"
});
Schema と JSON Schema
@effect/schema が生成するのは JSON Schema Draft-07 です(2020-12 ではありません)。
import { Schema, JSONSchema } from "@effect/schema";
const User = Schema.Struct({
id: Schema.String,
age: Schema.Number.pipe(Schema.positive())
});
// Generate JSON Schema (Draft-07)
const jsonSchema = JSONSchema.make(User);
// { "$schema": "http://json-schema.org/draft-07/schema#", ... }
// Decode (parse)
const user = Schema.decodeUnknownSync(User)(rawData);
// Encode
const json = Schema.encodeSync(User)(user);
可観測性と OpenTelemetry
Effect はネイティブの OpenTelemetry 統合を備えています。
import { NodeSdk } from "@effect/opentelemetry";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
// Add tracing to any effect
const traced = Effect.withSpan("processRequest")(myEffect);
// Logging with context
yield* Effect.log("Processing request");
yield* Effect.annotateLogs("requestId", "abc-123");
// FiberRef for fiber-local context propagation
const RequestId = FiberRef.unsafeMake<string>("");
yield* FiberRef.set(RequestId, "req-456");
Effect を使用しないほうがよい場合
| シナリオ | 推奨 |
|---|---|
| シンプルな MCP ツール(100 LOC 未満) | FastMCP または標準の SDK を使用してください |
| チームが関数型プログラミングに不慣れ | 学習曲線が急なため、NestJS を検討してください |
| バンドルサイズが重要 | Effect により、最低でも 15~25kb の gzip 圧縮後サイズが追加されます |
| 既存の NestJS/TypeORM コードベース | クラスベースの依存性注入とはインピーダンスミスマッチがあります |
MCP サーバーのパターン
型付きエラーを使ったツールハンドラー
const searchTool = Effect.gen(function* () {
const args = yield* parseArgs(input);
const db = yield* Database;
const results = yield* db.query(args.query);
return formatResults(results);
}).pipe(
Effect.catchTag("ParseError", () =>
Effect.fail({ code: -32602, message: "Invalid params" })
),
Effect.catchTag("DatabaseError", () =>
Effect.fail({ code: -32603, message: "Internal error" })
)
);
リクエストのスコープ管理
// Each MCP request gets its own scope
const handleRequest = (request: MCPRequest) =>
Effect.scoped(
Effect.gen(function* () {
// Resources acquired here auto-release when request completes
const tempFile = yield* createTempFile();
const result = yield* processRequest(request, tempFile);
return result;
})
);
リソース
コメント
Markdown 対応