Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Effect-TS をなぜ採用し、なぜ継続できたか? / Why Effect-TS? And...

Avatar for asa1984 asa1984
July 28, 2026
320

Effect-TS をなぜ採用し、なぜ継続できたか? / Why Effect-TS? And why do we keep using it?

Avatar for asa1984

asa1984

July 28, 2026

Transcript

  1. 自己紹介 asa1984 読み: アサヒ 2025年、株式会社HERP に新卒入社 『ジョブミル』の開発を経て、現在は『HERP AI Recruiter』を担当 やっていること

    フルスタックで機能開発・運用 最近はコードベースの治安維持活動に従事 Effect-TS をなぜ採用し、なぜ継続できたか? 実家の猫の尻尾 01
  2. 最初に: Effect-TS とは? 一言で言うと Promise を Effect に置き換える Effect<A, E,

    R>; // │ │ └─ Requirements: // │ └──────── Error: // └───────────────── 依存、環境、副作用 起こりうるエラー 成功時の値 Effect-TS をなぜ採用し、なぜ継続できたか? 04
  3. システム構成(簡略図) クライアント React SPA 外部システム 採用管理システム GraphQL バックエンド GraphQL サーバー

    データストア PostgreSQL 求人取り込み Effect-TS をなぜ採用し、なぜ継続できたか? OpenSearch バッチジョブ 07
  4. レイヤー別の Effect-TS の import 率 各レイヤーで effect / @effect/* を

    import しているファイルの割合 ユースケース PostgreSQL OpenSearch サーバー ドメイン Effect-TS をなぜ採用し、なぜ継続できたか? 96.3% 90.3% 72.7% 52.3% 31.4% 08
  5. ドメイン よく使う API domain Effect Schema Effect Option Schema Other

    Context.Tag Context Effect-TS をなぜ採用し、なぜ継続できたか? 基本的には通常の TypeScript で型や ロジックが定義されている。 09
  6. ドメイン: サービスのインターフェースを定義する 求人リポジトリのインターフェース // class JobRepository extends Context.Tag("JobRepository")< JobRepository, {

    find: (id: Job.ID) => Effect.Effect<Job.Entity | null>; store: (job: Job.Entity) => Effect.Effect<Job.Entity>; /* */ } >() {} 中略 Context.Tag を用いて依存のインターフェースを定義する Effect-TS をなぜ採用し、なぜ継続できたか? 10
  7. ユースケース よく使う API use-cases Effect Effect Other Effect.gen() Layer Match

    Effect-TS をなぜ採用し、なぜ継続できたか? リポジトリ全体で見ても `Effect.gen()` が最頻出の API になっている 11
  8. Promise と async / await const fetchUser: () => Promise<User>;

    const fetchPosts: (userId: UserID) => Promise<Post[]>; const main = async () => { const user = await fetchUser(); const posts = await fetchPosts(user.id); return { user, posts }; }; Effect-TS をなぜ採用し、なぜ継続できたか? 12
  9. Effect.gen() const fetchUser: () => Effect.Effect<User>; const fetchPosts: (userId: UserID)

    => Effect.Effect<Post[]>; const main = Effect.gen(function* () { const user = yield* fetchUser(); const posts = yield* fetchPosts(user.id); return { user, posts }; }); Effect-TS をなぜ採用し、なぜ継続できたか? 13
  10. Promise と Effect の対応 Promise then のチェーン async / await

    Effect と Effect.flatMap Effect.gen と yield* pipe Effect-TS をなぜ採用し、なぜ継続できたか? 14
  11. 合成 const program = Effect.gen(function* () { const suggestion =

    yield* findSuggestion({ id }); // SuggestionNotFound yield* removeSuggestion({ suggestion }); // SuggestionAlreadyRecommended return suggestion.id; }); の型。 すると勝手に と // program yield* Error Requirements type Program = Effect.Effect< Suggestion.ID, SuggestionNotFound | SuggestionAlreadyRecommended, Suggestion.Repository | Recommendation.Repository >; Effect-TS をなぜ採用し、なぜ継続できたか? が合成される。 15
  12. 永続化と検索サービス prisma Effect search Effect Effect Stream よく使う API Effect.promise()

    Layer.effect() Schema Schema Other Layer Effect-TS をなぜ採用し、なぜ継続できたか? 17
  13. Promise を Effect に変換する の // JobRepository find const find

    = (id: Job.ID) => Effect.promise(() => prisma.job.findUnique({ where: { id, organizationID } }), ); は Promise が reject したとき defect を発生させる defect: 回復不能な異常 Effect.promise Effect-TS をなぜ採用し、なぜ継続できたか? 18
  14. Two Types of Error API 型 Effect.promise Effect<A, never> Effect.tryPromise

    Effect<A, UnknownException> Repository では基本的に Effect.promise を使っている インフラのエラーはアプリケーションのロジックで回復しない判断をしている Effect-TS をなぜ採用し、なぜ継続できたか? 19
  15. アプリケーション GraphQL server Effect Effect Option Schema Layer Match Other

    Effect-TS をなぜ採用し、なぜ継続できたか? よく使う API Effect.catchTags() Effect.provide() Effect.run* Layer.* 20
  16. エラーハンドリング 成功 findSuggestion removeSuggestion SuggestionNotFound SuggestionAlready Recommended 失敗 GraphQL レスポンス

    レスポンス Effect.catchTags GraphQL エラー Effect.die プログラムの出口で合成されたエラーをパターンマッチし、適切な出力に変換する Effect-TS をなぜ採用し、なぜ継続できたか? 21
  17. 依存の構築と注入 依存の実体を にする // Layer const OpenSearchClientLive = Layer.succeed(OpenSearchClient, opensearchClient);

    // → Layer<OpenSearchClient, never, never> に注入する // Effect program.pipe(Effect.provide(OpenSearchClientLive)); // Effect<A, E, OpenSearchClient> → Effect<A, E, never> は Tag とその実装を受け取ってサービスを構成する になるまでプログラムは実行できない Layer.succeed R never が Effect-TS をなぜ採用し、なぜ継続できたか? 22
  18. 型で見える依存関係のグラフ // Layer<JobRepository, never, PrismaClient | Organization> const JobRepositoryLive =

    Layer.effect( JobRepository, Effect.gen(function* () { const prisma = yield* PrismaClient; const organization = yield* Organization; // /* */ }), ); 中略 テナント情報 組み立てに他のサービスが必要な場合は Layer.effect 必要な依存は Layer の 3 つ目の型引数に現れる Effect-TS をなぜ採用し、なぜ継続できたか? 23
  19. Effect.run* の // GraphQL resolver const exit = await program.pipe(

    Effect.provide(context.layers.global), // Effect.provide(Layer.succeed(Organization, context.organization)), Effect.runPromiseExit, ); 認証で確定した組織をここで注入する は純粋な値なので Effect.run* に渡すまで実行されない この例では runPromiseExit を使い、defect を安全にキャッチできるようにし ている Effect Effect-TS をなぜ採用し、なぜ継続できたか? 24
  20. スキーマ のレスポンスのスキーマ // API const Candidate = Schema.Struct({ birthday: Schema.optional(

    Schema.String.annotations({ jsonSchema: { format: "date", type: "string" }, }), ), /* */ }).annotations({ identifier: "Candidate" }); 中略 Zod や Valibot と同じ Standard Schema v1 に対応したスキーマを内包している Effect-TS をなぜ採用し、なぜ継続できたか? 26
  21. 可観測性 Observability 系の API がコア API として提供されている: ロガー トレース メトリクス

    Effect にはトレース用のメタデータを保存する領域が備わっている Effect-TS をなぜ採用し、なぜ継続できたか? 27
  22. 可観測性: 構造化ログの属性を付与する program.pipe( /* */ Effect.annotateLogs({ "graphql.field.name": info.fieldName, "organization.id": context.organization.id,

    "user.id": context.user.id, }), Effect.runPromiseExit, ); 中略 これ以降、program の内側で出るすべてのログの属性に ID が付く Effect-TS をなぜ採用し、なぜ継続できたか? 28
  23. 可観測性: span を作る Effect.gen(function* () { /* */ 中略 yield*

    Effect.annotateCurrentSpan({ "llm.model": model, "llm.input_tokens": usage.input_tokens, "llm.output_tokens": usage.output_tokens, }); }).pipe(Effect.withSpan("LLMClient.createMessages()")); Effect-TS をなぜ採用し、なぜ継続できたか? 29
  24. 可観測性: 出力先も注入する Layer.mergeAll( Logger.makeLayer(logger), // Tracer.layerGlobal, // OpenTelemetry /* */

    ); 中略 ログの出力先 の Tracer は Effect に注釈を与えるだけ 依存と同様、出力する実体は後から注入する annotateLogs , withSpan ※ ジョブミルでは Effect-TS 組み込みのロガー( Effect.log )ではなく winston をラップした自前のカスタムロガーを定義して差し 替えている。 Effect-TS をなぜ採用し、なぜ継続できたか? 30
  25. 並行処理: 構造化並行性 Effect.gen(function* () { const counts = yield* Effect.all(

    { advertisedJobs: count.advertisedJobs(query), jobs: count.jobs(query), manualJobs: count.manualJobs(query), sharedJobs: count.sharedJobs(query), }, { concurrency: "unbounded" }, ); }); どれかが失敗したら残りは自動で中断される Effect-TS をなぜ採用し、なぜ継続できたか? 31
  26. 並行処理: 並列度の制限 へ並列でリクエストを送る処理 // LLM Effect.forEach(jobs, extractSearchFields, { concurrency: llmConcurrency,

    }); 一度に大量のリクエストを送ってレート制限を超過しないように並列度を制限する Effect-TS をなぜ採用し、なぜ継続できたか? 32
  27. 並行処理: リトライ へリクエストを送る処理 // LLM invoke.pipe( Effect.retry({ // schedule: Schedule.jittered(Schedule.exponential(1_000,

    2)), times: 9, // while: (e) => e instanceof RateLimited || e instanceof ServiceUnavailable, }), ); 指数バックオフ 一時的なエラーの場合はリトライする Effect-TS をなぜ採用し、なぜ継続できたか? 33
  28. 付録: 集計方法と注意点 対象は git ls-files '*.ts' '*.tsx' の 5,890 ファイル。

    node_modules や生成物は含まない。 各ファイル内の import 文を解析し、そのファイルで Effect-TS 由来と確定した名 前空間についてのみ Namespace.member を数える。 @effect/vitest の it / expect / describe はテスト用のため集計対 象から除外している。 Effect-TS をなぜ採用し、なぜ継続できたか? 37