mcpField
E
effect-ts

effect-ts

github.com/joelhooks開発441.1k 回インストール23 週間前に更新
リポジトリ

effect-ts について

Effect v4 を使う TypeScript 開発者向けのスキルで、Effect コードの作成、レビュー、改修を支援します。公式ソースの確認を重視し、サービス、Schema、エラー、Layer、テストのパターンをまとめています。

主な機能

  1. 01Effect 公式ソースを優先して確認
  2. 02Effect.gen と Effect.fn の合成パターン
  3. 03ServiceMap.Service によるサービス定義
  4. 04Schema とブランド型によるドメインモデリング
  5. 05Layer 合成と Effect のテストパターン

活用シーン

  1. 01Effect プロジェクトでの機能実装
  2. 02現行ソースに基づく v4 API の確認
  3. 03サービス層と依存関係の構築
  4. 04Effect テストとエラー回復の作成

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/joelhooks/effectts-skills --skill effect-ts

インストール後、Claude Code で/effect-tsを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
cli.md
config.md
data-modeling.md
error-handling.md
http-clients.md
processes.md
schema-decisions.md
services-and-layers.md
setup.md
testing.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.KEYConfig.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 / undefinedOption.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、プロジェクト構成、モジュール設定

コメント