effect-ts について
effect-ts は、Effect ライブラリを使った開発を支援するスキルです。Effect のコードベースを扱う開発者向けに、型付きエラー処理、依存性注入、合成、リソース管理、並行処理、テストのパターンを説明します。
主な機能
- 01型付きエラー処理のパターン
- 02依存性注入の Services と Layers
- 03Effect の合成とリソーススコープ
- 04並行処理、ストリーム、スケジューリングの指針
- 05テストとコードパターンの調査
活用シーン
- 01Effect プログラムのエラー処理を設計
- 02アプリケーションにサービスとレイヤーを追加
- 03非同期処理とリソースを管理
- 04Effect のテストパターンを適用
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ sablier-labs/ agent-skills --skill effect-tsインストール後、Claude Code で/effect-tsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/critical-rules.mdeffect-atom.mdnext-js.mdoption-null.mdstreams.mdtesting.md詳細
Effect-TS エキスパート
エラー処理、依存性注入、合成可能性、テストパターンなど、Effect ライブラリを使った関数型プログラミングに関する専門的なガイダンスです。
前提条件の確認
Effect 関連の作業を始める前に、Effect-TS のソースコードが ~/.effect に存在することを確認してください。
存在しない場合は、直ちに作業を中止してユーザーに伝えてください。 続行する前にクローンしてください。
git clone https://github.com/Effect-TS/effect.git ~/.effect
調査戦略
Effect-TS には、同じタスクを実現するさまざまな方法があります。Effect のパターンを扱う際は、特に中程度から高い複雑さのタスクでは、Task ツールを使って調査エージェントを起動し、ベストプラクティスを積極的に調査してください。
調査ソース(優先順位順)
-
まずコードベースのパターンを確認する — 実装の前に、現在のプロジェクト内で類似のパターンを調べてください。コードベースに Effect のパターンが存在する場合は、一貫性を保つためにそれに従ってください。パターンが存在しない場合は、この手順を省略してください。
-
Effect のソースコード — 複雑な型エラー、動作が不明確な場合、または実装の詳細を確認する場合は、
~/.effect/packages/effect/src/にある Effect のソースを調べてください。ここには、Effect の中核となるロジックとモジュールが含まれています。
調査が必要な場面
高優先度(必ず調査):
- Services、Layers、または複雑な依存性注入を実装する場合
- 複数のエラー型や複雑なエラー階層を伴うエラー処理
- ストリームベースの操作やリアクティブなパターン
- スコープ付きエフェクトとクリーンアップを用いたリソース管理
- 並行/並列処理やパフォーマンスが重要なコード
- テストパターン、特に不慣れなテストシナリオ
中優先度(複雑な場合に調査):
- 命令型コード(try-catch、promises)を Effect のパターンにリファクタリングする場合
- 新しいサービス依存関係を追加する場合、またはサービスレイヤーを再構成する場合
- カスタムエラー型を作成する場合、または既存のエラー階層を拡張する場合
- 外部システム(データベース、API、サードパーティサービス)と連携する場合
調査方法
- 関連する複数のパターンを調査する場合は、複数の Task エージェントを並行して起動してください
- 気の利いた最適化よりも、標準的で読みやすく、保守しやすい解決策を見つけることを重視してください
- 提案された方法を、既存のコードベースのパターン(存在する場合)と照らし合わせ、一貫性を確認してください
- 複数の方法が考えられる場合は、最も慣用的な Effect-TS の解決策を見つけるために調査してください
コードベースのパターンの発見
Effect を使用するプロジェクトで作業する場合、新しいコードを実装する前に既存のパターンを確認してください。
- Effect のインポートを検索する —
'effect'からインポートしているファイルを探し、既存の使用方法を把握します - サービスのパターンを特定する — プロジェクト内で Services と Layers がどのように構成されているかを確認します
- エラー処理の規約を確認する — エラーの定義方法と伝播方法を確認します
- テストパターンを調べる — プロジェクト内で Effect のコードがどのようにテストされているかを確認します
コードベースに Effect のパターンが存在しない場合は、Effect のソースとサンプルにある標準的なパターンを使って進めてください。 コードベースにパターンがないことを理由に、作業を止めないでください。
Effect の原則
Effect のコードを書く際は、次の基本原則を適用してください。
エラー処理
- 例外をスローする代わりに、Effect の型付きエラーシステムを使用します
- 適切にエラーが伝播する、説明的なエラー型を定義します
- エラー制御フローには
Effect.fail、Effect.catchTag、Effect.catchAllを使用します - 禁止されているパターンについては、
./references/critical-rules.mdを参照してください
依存性注入
- Services と Layers を使って依存性注入を実装します
Context.Tagを使ってサービスを定義しますLayer.merge、Layer.provideを使ってレイヤーを合成しますEffect.provideを使って依存関係を注入します
合成可能性
- Effect の合成可能性を活用して、複雑な操作を実現します
- 適切なコンストラクターを使用します:
Effect.succeed、Effect.fail、Effect.tryPromise、Effect.try - スコープ付きエフェクトを使って、適切なリソース管理を行います
Effect.flatMap、Effect.map、Effect.tapを使って操作を連鎖させます
コード品質
- Effect の型システムを活用した、型安全なコードを書きます
- 読みやすい逐次処理には
Effect.genを使用します - Effect のテストユーティリティを使って、適切なテストパターンを実装します
- テレメトリーの自動記録とスタックトレースの改善には
Effect.fn()を優先します
重要なルール
Effect コードを書く前に ./references/critical-rules.md を読み、内容を理解してください。主なガイドラインは以下のとおりです。
- 効果がありません: Effect.gen 内の try-catch(Effect の失敗はスローされません)
- 避けてください: 型アサーション(as never/any/unknown)
- 推奨: エラーには
return yield*パターンを使います(終了を明示できます)
よくある失敗パターン
問題が頻発するパターンへのクイックリンク:
- SubscriptionRef のバージョン不一致 —
unsafeMake is not a function→ クイックリファレンス - キャンセルと失敗 — 割り込みはエラーではありません → エラーの分類
- Option と null — 内部では Option を使い、境界では null を使います → option-null.md
- Stream のバックプレッシャー — 無限ストリームは停止しません → streams.md
解決策の説明
解決策を提示する際は、使用している Effect-TS の概念と、それが特定のユースケースに適している理由を説明してください。ドキュメントで扱われていないパターンに遭遇した場合は、既存のコードベースにパターンがあれば、それとの一貫性を保ちながら改善案を提示してください。
クイックリファレンス
Effect の作成
Effect.succeed(value) // Wrap success value
Effect.fail(error) // Create failed effect
Effect.tryPromise(fn) // Wrap promise-returning function
Effect.try(fn) // Wrap synchronous throwing function
Effect.sync(fn) // Wrap synchronous non-throwing function
Effect の合成
Effect.flatMap(effect, fn) // Chain effects
Effect.map(effect, fn) // Transform success value
Effect.tap(effect, fn) // Side effect without changing value
Effect.all([...effects]) // Run effects (concurrency configurable)
Effect.forEach(items, fn) // Map over items with effects
// Collect ALL errors (not just first)
Effect.all([e1, e2, e3], { mode: "validate" }) // Returns all failures
// Partial success handling
Effect.partition([e1, e2, e3]) // Returns [failures, successes]
エラー処理
// Define typed errors with Data.TaggedError (preferred)
class UserNotFoundError extends Data.TaggedError("UserNotFoundError")<{
userId: string
}> {}
// Direct yield of errors (no Effect.fail wrapper needed)
Effect.gen(function* () {
if (!user) {
return yield* new UserNotFoundError({ userId })
}
})
Effect.catchTag(effect, tag, fn) // Handle specific error tag
Effect.catchAll(effect, fn) // Handle all errors
Effect.result(effect) // Convert to Exit value
Effect.orElse(effect, alt) // Fallback effect
エラーの分類
適切に処理できるよう、エラーを分類します。
| カテゴリー | 例 | 処理 |
|---|---|---|
| 想定される拒否 | ユーザーによるキャンセル、拒否 | 適切に終了し、再試行しない |
| ドメインエラー | 検証、ビジネスルール | ユーザーに表示し、再試行しない |
| 欠陥 | バグ、アサーション | ログ記録とアラート、調査 |
| 割り込み | Fiber のキャンセル、タイムアウト | クリーンアップし、再試行する場合があります |
| 不明/外部 | スローされた例外 | 境界で正規化 |
// Pattern: Normalize unknown errors at boundary
const safeBoundary = Effect.catchAllDefect(effect, (defect) =>
Effect.fail(new UnknownError({ cause: defect }))
)
// Pattern: Catch user-initiated cancellations separately
Effect.catchTag(effect, "UserCancelledError", () => Effect.succeed(null))
// Pattern: Handle interruptions differently from failures
Effect.onInterrupt(effect, () => Effect.log("Operation cancelled"))
パターンマッチ(Match モジュール)
タグ付きユニオンや複雑な条件分岐で使用する、既定の分岐ツールです。
import { Match } from "effect"
// Type-safe exhaustive matching on tagged errors
const handleError = Match.type<AppError>().pipe(
Match.tag("UserCancelledError", () => null), // Expected rejection
Match.tag("ValidationError", (e) => e.message), // Domain error
Match.tag("NetworkError", () => "Connection failed"), // Retryable
Match.exhaustive // Compile error if case missing
)
// Replace nested catchTag chains
// BEFORE: effect.pipe(catchTag("A", ...), catchTag("B", ...), catchTag("C", ...))
// AFTER:
Effect.catchAll(effect, (error) =>
Match.value(error).pipe(
Match.tag("A", handleA),
Match.tag("B", handleB),
Match.tag("C", handleC),
Match.exhaustive
)
)
// Match on values (cleaner than if/else)
const describe = Match.value(status).pipe(
Match.when("pending", () => "Loading..."),
Match.when("success", () => "Done!"),
Match.orElse(() => "Unknown")
)
サービスとレイヤー
// Pattern 1: Context.Tag (implementation provided separately via Layer)
class MyService extends Context.Tag("MyService")<MyService, { ... }>() {}
const MyServiceLive = Layer.succeed(MyService, { ... })
Effect.provide(effect, MyServiceLive)
// Pattern 2: Effect.Service (default implementation bundled)
class UserRepo extends Effect.Service<UserRepo>()("UserRepo", {
effect: Effect.gen(function* () {
const db = yield* Database
return { findAll: db.query("SELECT * FROM users") }
}),
dependencies: [Database.Default], // Optional service dependencies
accessors: true // Auto-generate method accessors
}) {}
Effect.provide(effect, UserRepo.Default) // .Default layer auto-generated
// Use UserRepo.DefaultWithoutDependencies when deps provided separately
// Effect.Service with parameters (3.16.0+)
class ConfiguredApi extends Effect.Service<ConfiguredApi>()("ConfiguredApi", {
effect: (config: { baseUrl: string }) =>
Effect.succeed({ fetch: (path: string) => `${config.baseUrl}/${path}` })
}) {}
// Pattern 3: Context.Reference (defaultable tags - 3.11.0+)
class SpecialNumber extends Context.Reference<SpecialNumber>()(
"SpecialNumber",
{ defaultValue: () => 2048 }
) {}
// No Layer required if default value suffices
// Pattern 4: Context.ReadonlyTag (covariant - 3.18.0+)
// Use for functions that consume services without modifying the type
function effectHandler<I, A, E, R>(service: Context.ReadonlyTag<I, Effect.Effect<A, E, R>>) {
// Handler can use service in a covariant position
}
ジェネレーターパターン
Effect.gen(function* () {
const a = yield* effectA;
const b = yield* effectB;
if (error) {
return yield* Effect.fail(new MyError());
}
return result;
});
// Effect.fn - automatic tracing and telemetry (preferred for named functions)
const fetchUser = Effect.fn("fetchUser")(function* (id: string) {
const db = yield* Database
return yield* db.query(id)
})
// Creates spans, captures call sites, provides better stack traces
リソース管理
Effect.acquireUseRelease(acquire, use, release) // Bracket pattern
Effect.scoped(effect) // Scope lifetime to effect
Effect.addFinalizer(cleanup) // Register cleanup action
Duration
Effect では、DurationInput が期待される場所ならどこでも、人が読みやすい期間文字列を受け付けます。
// String syntax (preferred) - singular or plural forms work
Duration.toMillis("5 minutes") // 300000
Duration.toMillis("1 minute") // 60000
Duration.toMillis("30 seconds") // 30000
Duration.toMillis("100 millis") // 100
// Verbose syntax (avoid)
Duration.toMillis(Duration.minutes(5)) // Same result, more verbose
// Common units: millis, seconds, minutes, hours, days, weeks
// Also: nanos, micros
スケジューリング
Effect.retry(effect, Schedule.exponential("100 millis")) // Retry with backoff
Effect.repeat(effect, Schedule.fixed("1 second")) // Repeat on schedule
Schedule.compose(s1, s2) // Combine schedules
状態管理
Ref.make(initialValue) // Mutable reference
Ref.get(ref) // Read value
Ref.set(ref, value) // Write value
Deferred.make<E, A>() // One-time async value
SubscriptionRef(リアクティブな参照)
// WARNING: Never use unsafeMake - it may not exist in your Effect version.
// If you see "unsafeMake is not a function", use the safe API below.
SubscriptionRef.make(initial) // Create reactive reference (safe)
SubscriptionRef.get(ref) // Read current value
SubscriptionRef.set(ref, value) // Update value (notifies subscribers)
SubscriptionRef.changes(ref) // Stream of value changes
// React integration (effect-atom pattern)
const ref = yield* SubscriptionRef.make<User | null>(null)
// Hook reads: useSubscriptionRef(ref) — returns current value or null
// Handle null explicitly in components
並行処理
Effect.fork(effect) // Run in background fiber
Fiber.join(fiber) // Wait for fiber result
Effect.race(effect1, effect2) // First to complete wins
Effect.all([...effects], { concurrency: "unbounded" })
設定と環境変数
import { Config, ConfigProvider, Effect, Layer, Redacted } from "effect"
// Basic config values
const port = Config.number("PORT") // Required number
const host = Config.string("HOST").pipe( // Optional with default
Config.withDefault("localhost")
)
// Sensitive values (masked in logs)
const apiKey = Config.redacted("API_KEY") // Returns Redacted<string>
const secret = Redacted.value(yield* apiKey) // Unwrap when needed
// Nested configuration with prefix
const dbConfig = Config.all({
host: Config.string("HOST"),
port: Config.number("PORT"),
name: Config.string("NAME"),
}).pipe(Config.nested("DATABASE")) // DATABASE_HOST, DATABASE_PORT, etc.
// Using config in effects
const program = Effect.gen(function* () {
const p = yield* Config.number("PORT")
const key = yield* Config.redacted("API_KEY")
return { port: p, apiKey: Redacted.value(key) }
})
// Custom config provider (e.g., from object instead of env)
const customProvider = ConfigProvider.fromMap(
new Map([["PORT", "3000"], ["API_KEY", "secret"]])
)
const withCustomConfig = Effect.provide(
program,
Layer.setConfigProvider(customProvider)
)
// Config validation and transformation
const validPort = Config.number("PORT").pipe(
Config.validate({
message: "Port must be between 1 and 65535",
validation: (n) => n >= 1 && n <= 65535,
})
)
配列操作
import { Array as Arr, Order } from "effect"
// Sorting with built-in orderings (accepts any Iterable)
Arr.sort([3, 1, 2], Order.number) // [1, 2, 3]
Arr.sort(["b", "a", "c"], Order.string) // ["a", "b", "c"]
Arr.sort(new Set([3n, 1n, 2n]), Order.bigint) // [1n, 2n, 3n]
// Sort by derived value
Arr.sortWith(users, (u) => u.age, Order.number)
// Sort by multiple criteria
Arr.sortBy(
users,
Order.mapInput(Order.number, (u: User) => u.age),
Order.mapInput(Order.string, (u: User) => u.name)
)
// Built-in orderings: Order.string, Order.number, Order.bigint, Order.boolean, Order.Date
// Reverse ordering: Order.reverse(Order.number)
ユーティリティ関数
import { constVoid as noop } from "effect/Function"
// constVoid returns undefined, useful as a no-operation callback
noop() // undefined
// Common use cases:
Effect.tap(effect, noop) // Ignore value, just run effect
Promise.catch(noop) // Swallow errors
eventEmitter.on("event", noop) // Register empty handler
非推奨項目
BigDecimal.fromNumber— 代わりにBigDecimal.unsafeFromNumberを使用してください(3.11.0+)Schema.annotations()— 設定済みの識別子アノテーションを削除するようになりました。識別子はスキーマのast参照のみに関連付けられます(3.17.10)
追加リソース
Effect のローカルリソース
~/.effect/packages/effect/src/— Effect のコアモジュールと実装
外部リソース
参考ファイル
./references/critical-rules.md— 禁止パターンと必須規約./references/effect-atom.md— React 向け Effect-Atom のリアクティブな状態管理./references/next-js.md— Effect と Next.js 15+ App Router の統合パターン./references/option-null.md— Option と null の境界パターン./references/streams.md— Stream のパターンとバックプレッシャーに関する注意点./references/testing.md— Vitest による決定論的なテストパターン