effect-ts について
Effect v4 を使う TypeScript 開発者向けのスキルで、Effect コードの作成、レビュー、改修を支援します。公式ソースの確認を重視し、サービス、Schema、エラー、Layer、テストのパターンをまとめています。
主な機能
- 01Effect 公式ソースを優先して確認
- 02Effect.gen と Effect.fn の合成パターン
- 03ServiceMap.Service によるサービス定義
- 04Schema とブランド型によるドメインモデリング
- 05Layer 合成と Effect のテストパターン
活用シーン
- 01Effect プロジェクトでの機能実装
- 02現行ソースに基づく v4 API の確認
- 03サービス層と依存関係の構築
- 04Effect テストとエラー回復の作成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ joelhooks/ effectts-skills --skill effect-tsインストール後、Claude Code で/effect-tsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/cli.mdconfig.mddata-modeling.mderror-handling.mdhttp-clients.mdprocesses.mdschema-decisions.mdservices-and-layers.mdsetup.mdtesting.md詳細
Effect-TS(v4)
effect-solutions と Effect のソースコード に基づくパターンです。最新の v4 API を対象としています。
ソース優先のルール
Effect を使用する(パッケージ/依存関係ファイルに effect または @effect/* が含まれる)リポジトリで作業する場合は、Effect コードの作成、レビュー、リファクタリングの前に、公式の Effect ソースを参照してください。古い記憶、ブログ記事、概要レベルのドキュメントだけに頼らないでください。
effect_sourceツールが利用できる場合は、独自のシェルコマンドではなく、status、hydrate、searchに使用してください。- まず、リポジトリ内に
.agent-sources/effect/の浅いソースミラーがあるか確認してください。 - ない場合は、Effect の作業を始める前に作成してください:
mkdir -p .agent-sources && git clone --depth 1 --filter=blob:none https://github.com/effect-ts/effect.git .agent-sources/effect - ミラーを製品のコミットに含めないでください。必要であれば、Joel が明示的にコミットを希望しない限り、プロジェクトの
.gitignoreではなく.git/info/excludeに.agent-sources/を追加してください。 - Effect のベストプラクティスとして紹介する前に、ミラー内、特に
packages/effect/src/とパッケージのテスト/サンプルを検索し、最新のパターンや API を確認してください。
ローカルのソース参照
- リポジトリ内の Effect ソースミラー(現在の作業での標準):
.agent-sources/effect/ - effect-solutions(ベストプラクティス、ドキュメント、サンプル):
~/Code/kitlangton/effect-solutions/ - フォールバック用のグローバル Effect モノレポ:
~/Code/effect-ts/effect/ - 実装をソースから検索する場合:
grep -r "pattern" .agent-sources/effect/packages/effect/src/
Effect.gen と Effect.fn
Effect.gen は、順次実行される読みやすい合成を提供します(Effect における async/await のようなものです):
import { Effect } from "effect"
const program = Effect.gen(function* () {
const data = yield* fetchData
yield* Effect.logInfo(`Processing: ${data}`)
return yield* processData(data)
})
Effect.fn は、呼び出し箇所のトレースと名前付きスパンを追加します。すべてのサービスメソッドで使用してください:
const processUser = Effect.fn("processUser")(function* (userId: string) {
yield* Effect.logInfo(`Processing user ${userId}`)
const user = yield* getUser(userId)
return yield* processData(user)
})
// Second argument for cross-cutting concerns (retry, timeout)
const fetchWithRetry = Effect.fn("fetchWithRetry")(
function* (url: string) {
const data = yield* fetchData(url)
return yield* processData(data)
},
flow(
Effect.retry(Schedule.recurs(3)),
Effect.timeout("5 seconds")
)
)
ServiceMap.Service
一意のタグと型付きインターフェースを持つクラスとしてサービスを定義します:
import { Effect, ServiceMap } from "effect"
class Database extends ServiceMap.Service<
Database,
{
readonly query: (sql: string) => Effect.Effect<unknown[]>
readonly execute: (sql: string) => Effect.Effect<void>
}
>()("@app/Database") {}
すべてのメソッドに Effect.fn を使用し、Layer.effect または Layer.sync で実装します:
import { Effect, Layer } from "effect"
class Users extends ServiceMap.Service<
Users,
{
readonly findById: (id: UserId) => Effect.Effect<User, UserNotFoundError>
readonly all: () => Effect.Effect<readonly User[]>
}
>()("@app/Users") {
static readonly layer = Layer.effect(
Users,
Effect.gen(function* () {
const http = yield* HttpClient.HttpClient
const findById = Effect.fn("Users.findById")(function* (id: UserId) {
const response = yield* http.get(`/users/${id}`)
return yield* HttpClientResponse.schemaBodyJson(User)(response)
})
const all = Effect.fn("Users.all")(function* () {
const response = yield* http.get("/users")
return yield* HttpClientResponse.schemaBodyJson(Schema.Array(User))(response)
})
return { findById, all }
})
)
}
ルール:
- タグ識別子は一意でなければなりません。
@app/ServiceNameのパターンを使用してください - サービスメソッドの
Rはneverにしてください(依存関係はメソッドシグネチャではなく Layer 経由で渡します) readonlyプロパティを使用してください
サービス駆動開発、テスト用レイヤー、レイヤーのメモ化、完全な合成パターンについては、references/services-and-layers.md を参照してください。
Schema.Class とブランド型
ドメインレコードには Schema.Class を使用します。すべてのエンティティ ID とドメインプリミティブにブランドを付けます:
import { Schema } from "effect"
const UserId = Schema.String.pipe(Schema.brand("UserId"))
type UserId = typeof UserId.Type
const Email = Schema.String.pipe(Schema.brand("Email"))
type Email = typeof Email.Type
class User extends Schema.Class("User")({
id: UserId,
name: Schema.String,
email: Email,
createdAt: Schema.Date,
}) {
get displayName() { return `${this.name} (${this.email})` }
}
// Construct with makeUnsafe for brands
const userId = UserId.makeUnsafe("user-123")
バリアント(OR 型)には Schema.TaggedClass と Schema.Union を使用します:
import { Match, Schema } from "effect"
class Success extends Schema.TaggedClass("Success")("Success", {
value: Schema.Number,
}) {}
class Failure extends Schema.TaggedClass("Failure")("Failure", {
error: Schema.String,
}) {}
const Result = Schema.Union([Success, Failure])
type Result = typeof Result.Type
// Exhaustive pattern matching
const render = (r: Result) => Match.valueTags(r, {
Success: ({ value }) => `Got: ${value}`,
Failure: ({ error }) => `Error: ${error}`,
})
JSON エンコーディング、Schema.Literals、バリデーション、完全なパターンについては、references/data-modeling.md を参照してください。
Schema.TaggedErrorClass
ドメインエラーは Schema.TaggedErrorClass で定義します。これらは yield 可能なため、Effect.fail は必要ありません:
import { Schema } from "effect"
class UserNotFoundError extends Schema.TaggedErrorClass("UserNotFoundError")(
"UserNotFoundError",
{ userId: UserId, message: Schema.String }
) {}
// Yieldable: yield directly in generators
const getUser = Effect.fn("getUser")(function* (id: UserId) {
const user = yield* findUser(id)
if (!user) yield* new UserNotFoundError({ userId: id, message: "Not found" })
return user
})
catchTag/catchTags で復旧します:
// Single tag
const recovered = program.pipe(
Effect.catchTag("UserNotFoundError", (e) =>
Effect.succeed(`User ${e.userId} missing`)
)
)
// Multiple tags
const recovered2 = program.pipe(
Effect.catchTags({
UserNotFoundError: (e) => Effect.succeed("not found"),
ValidationError: (e) => Effect.succeed("invalid"),
})
)
欠陥、Schema.Defect、復旧パターンについては、references/error-handling.md を参照してください。
Layer の合成
Layer.provideMerge(段階的でフラットな型)と Layer.merge(並列)でレイヤーを合成します:
import { Effect, Layer } from "effect"
// Compose layers for the app
const appLayer = UserService.layer.pipe(
Layer.provideMerge(DatabaseLayer),
Layer.provideMerge(LoggerLayer),
Layer.provideMerge(ConfigLayer),
)
// Provide once at the entry point
const main = program.pipe(Effect.provide(appLayer))
Effect.runPromise(main)
主なルール:
- パラメーター化されたレイヤーは定数に格納します(レイヤーのメモ化は参照の同一性に基づきます)
- アプリケーションのエントリーポイントで一度だけ提供し、コード全体に分散させないでください
- 同期的な実装には
Layer.syncを、Effect を伴う実装にはLayer.effectを使用してください
テストのクイックスタート
import { describe, expect, it } from "@effect/vitest"
import { Effect, Layer } from "effect"
it.effect("queries database", () =>
Effect.gen(function* () {
const db = yield* Database
const results = yield* db.query("SELECT *")
expect(results.length).toBe(2)
}).pipe(Effect.provide(Database.testLayer))
)
- Effect ベースのテストには
it.effectを使用します(TestClock を備えた TestContext が提供されます) - 実際の時間/実際のクロックには
it.liveを使用します - 状態の漏洩を防ぐため、テストごとに新しいレイヤーを提供します
- スイート全体で高コストなリソースを共有する場合に限り、
it.layerを使用します
完全な実例と高度なパターンについては、references/testing.md を参照してください。
インストルメンテーションに Pipe を使用
const program = fetchData.pipe(
Effect.timeout("5 seconds"),
Effect.retry(Schedule.exponential("100 millis").pipe(
Schedule.compose(Schedule.recurs(3))
)),
Effect.tap((data) => Effect.logInfo(`Fetched: ${data}`)),
Effect.withSpan("fetchData"),
)
アンチパターン
| してはいけないこと | 代わりに行うこと |
|---|---|
console.log(...) | 構造化データとともに Effect.log(...) を使用します |
process.env.KEY | Config.string("KEY") または Config.redacted("KEY") を使用します |
Effect.gen 内での throw new Error() | yield* new TaggedError({...}) または Effect.fail(...) を使用します |
サービス内での Effect.runSync(...) | すべてを Effectful に保ちます |
型情報が失われる Effect.catchAll(() => ...) | Effect.catchTag または Effect.catchTags を使用します |
ドメイン型での null / undefined | Option.match を使う Option<T> を使用します |
Option.getOrThrow(...) | Option.match({ onNone, onSome }) または Option.getOrElse を使用します |
Effect.Service(v3) | ServiceMap.Service(v4)を使用します |
Schema.TaggedError<T>()(v3) | Schema.TaggedErrorClass("Tag")("Tag", {...})(v4)を使用します |
Effect.provide の呼び出しを散在させること | アプリケーションのエントリーポイントで一度だけ提供します |
| パラメーター化されたレイヤーコンストラクターをインラインで呼び出すこと | レイヤーを定数に格納します(メモ化) |
参考ファイル
より詳しいパターンについては、必要に応じて以下を読み込んでください。
- サービスとレイヤー: ServiceMap.Service、サービス駆動開発、テストレイヤー、レイヤーのメモ化、provide と provideMerge
- データモデリング: Schema.Class、ブランド型、バリアント、Match.valueTags、JSON エンコーディング
- スキーマの選択: Schema.Class、Struct、TaggedClass の選択フローチャート、移行パターン
- エラーハンドリング: Schema.TaggedErrorClass、catch/catchTag/catchTags、欠陥、Schema.Defect、TypeId/refail パターン
- テスト: @effect/vitest のセットアップ、it.effect/it.live/it.layer、TestClock、Effect.flip、FiberRef の分離、実例
- HTTP クライアント: HttpClient、リクエストの構築、レスポンスのデコード、ミドルウェア、リトライ、型付き API サービス
- CLI: Command.make、Arguments、Flags、サブコマンド、タスクマネージャーの実例
- 設定: Config モジュール、スキーマ検証、ConfigProvider、Redacted、設定レイヤー
- プロセスとスコープ: Fork の型、Scope.extend、子プロセス用の Command、終了可能なバックグラウンドタスク
- セットアップ: tsconfig、Effect Language Service、プロジェクト構成、モジュール設定