Skip to content
Published on

Effect-TS 徹底解説 — TypeScriptでエフェクトシステム・DI・リソース・並行性をひとつのライブラリで解く

Authors

プロローグ — Promiseだけでは答えにならない瞬間

次のコードを見てほしい。TypeScriptでよく見る形だ。

async function chargeCustomer(userId: string, amount: number): Promise<Receipt> {
  const user = await db.getUser(userId)
  const card = await stripe.getDefaultCard(user.stripeId)
  const charge = await stripe.charge(card, amount)
  await db.saveReceipt(charge)
  await mailer.sendReceipt(user.email, charge)
  return toReceipt(charge)
}

6行に await が5回。動く — ハッピーパスでは。だがこの関数のシグネチャが伝えるのは次の2つだけだ。

  1. Promise を返す。
  2. 成功時には Receipt が手に入る。

伝えていないこと:

  • どんなエラーが投げられるか — すべて Error に押し込まれる。
  • どの依存が必要か — dbstripemailer はクロージャに吸い込まれている。
  • 途中で落ちたら課金は通って領収書だけ送られないという状態になり得るか。
  • キャンセルがどう伝播するか(AbortSignal はどこに?)。
  • ステージを並列化したら何が起きるか。

リファクタが怖い理由はここにある。シグネチャの口数が少なすぎる。**エフェクトシステム(effect system)**は、その無口さを型で埋める発想だ。Haskell の IO、Scala の ZIO、F# のコンピュテーション式、Rust の Result と明示的な所有権 — みな同じ家族だ。

TypeScript 陣営でその椅子に座るライブラリが Effect-TS だ。2026年時点で安定した3.x、fp-ts の著者 Giulio Canti が合流し fp-ts を maintenance mode に送ったそのライブラリ。本稿は Effect-TS を解剖する — Effect<R, E, A>、演算、エラー、DI、リソース、スキーマ、ストリーム、Fiber まで。そして正直に使うべきでない場面も。


1章 · Effect型 — コンテキスト・エラー・値を一度に追跡する

Effect-TS のすべては、たった一つの型から始まる。

type Effect<A, E, R>

3つのパラメータが何を意味するかが核心だ。

位置名前意味
ASuccess成功時に返る値の型
EError / Failureこの effect が予期する失敗の型(ユニオン可)
RRequirements (Context)実行に必要な環境のサービス

読み方: 「Effect は環境 R の上で動き、E で失敗するか A で成功する」。Promise と比べると違いがはっきりする。

// Promise — 成功型しか見えない
function foo(): Promise<Receipt>

// Effect — 環境・失敗・成功がすべてシグネチャに現れる
function foo(): Effect.Effect<Receipt, StripeError | DbError, DbService | StripeService | Mailer>

これは見た目だけのようでいて、リファクタ安全性の次元が違う。関数が新しいエラーを投げ始めれば型が壊れ、関数が新しい依存を要求し始めれば呼び出し側で型エラーが出る。Promise はこれらを黙って素通りさせる。

エフェクト追跡の核: 「この関数が何をできるか」が型に刻まれる。呼び出し側はシグネチャだけで「ここで何が壊れうるか、何が必要か」を知る。

Effect<A, E, R> は**記述(description)**だ。即時実行されるのではなく、「この環境で実行されれば、こう振る舞う」というレシピである。最後に Effect.runPromise のようなインタプリタで一度だけ実行する。この遅延(lazy)モデルが、並行性・リトライ・キャンセルを安全に合成できる土台になる。


2章 · 基本演算 — map, flatMap, zip, race, all

Effect はモナドだ。出発点は2つの演算。

  • Effect.succeed(a)A を effect で包む。
  • Effect.flatMap(eff, a => effB)A が出たら次の effect を作る。

これだけで全シーケンスが表現可能。だが毎回 flatMap を書くのは疲れる。Effect-TS は Effect.gen でジェネレータベースのコルーチン記法を提供する — 実質的に async/await のような見た目で effect をシーケンス化する。

import { Effect } from "effect"

const program = Effect.gen(function* () {
  const user = yield* getUser(userId)
  const card = yield* getDefaultCard(user.stripeId)
  const charge = yield* chargeCard(card, amount)
  yield* saveReceipt(charge)
  yield* sendReceipt(user.email, charge)
  return toReceipt(charge)
})

yield*await のように見えるが、型システムが運ぶ情報量は段違いだ。各 yield* のたびに RE が積み重なり、最終シグネチャは全依存と全失敗の自動合成ユニオンになる。

主な演算

  • Effect.map(eff, fn) — 成功値を変換。
  • Effect.flatMap(eff, fn) — 成功値で次の effect へ。
  • Effect.zip(a, b) — 両方成功で成功、結果はタプル。
  • Effect.all([a, b, c]) — 複数を合成。並行度・fail-fast/全収集オプションあり。
  • Effect.race(a, b) — 先に完了した方を採用。負けた方は自動キャンセル
  • Effect.either(eff) — 失敗も値で受け取る(Either 変換)。
  • Effect.catchAll(eff, e => recover) — 失敗回復。
  • Effect.timeout(eff, "5 seconds") — タイムアウト。
  • Effect.retry(eff, schedule)Schedule でリトライ。

Effect.all の並行性オプションは強力だ。

Effect.all([fetchA, fetchB, fetchC], { concurrency: "unbounded" })
Effect.all(tasks, { concurrency: 8, mode: "either" })

mode: "either" は一部失敗しても残りを最後まで集める。Promise.allSettled に似ているが、型は正確で、並行度上限がライブラリレベルで強制される。


3章 · エラー — failuresとdefectsは別物

エフェクトシステムで一番高くつく教訓: すべての失敗が同じ種類ではない。

Effect は2つを明示的に区別する。

種類意味型に
Failure予期した失敗E に出現UserNotFoundPaymentDeclinedRateLimited
Defect想定外のバグE に出ないnull 参照、無限ループ、システム停止

ドメイン失敗は型に書いて回復する。バグは握りつぶさず上流へ流す — 握ると本物のバグが隠れる。この区別が曖昧だと try/catch 一発で本物のバグを呑み込む。

ドメインエラーの定義

import { Data } from "effect"

class UserNotFound extends Data.TaggedError("UserNotFound")<{
  userId: string
}> {}

class PaymentDeclined extends Data.TaggedError("PaymentDeclined")<{
  reason: string
  code: number
}> {}

Data.TaggedError は判別可能ユニオンのためのビルダ。各エラーに _tag が刻まれ、switch で絞り込みやすい。

const recovered = program.pipe(
  Effect.catchTags({
    UserNotFound: (e) => Effect.succeed(guestReceipt),
    PaymentDeclined: (e) => logAndNotify(e),
  }),
)

ここで強力なのは: catchTags で一部だけ処理すると、残ったエラーはシグネチャに残るということだ。ケースを全部処理していなければシグネチャが暴く。try/catch にこの保証はない。

Promise vs Effect — 同じ仕事

Promise:

async function charge() {
  try {
    const u = await getUser(id)
    return await stripe.charge(u, amt)
  } catch (e) {
    // e の型は unknown。どのエラー? 分からない。
    throw e
  }
}

Effect:

const charge = Effect.gen(function* () {
  const u = yield* getUser(id)            // E: UserNotFound
  return yield* stripeCharge(u, amt)      // E: StripeError
})
// 推論: Effect<Receipt, UserNotFound | StripeError, R>

回復するとき、どのエラーを扱っているかコンパイラが正確に教えてくれる。新たな失敗を加えれば、未処理のケースが即座に光る。


4章 · 依存注入 — Context.TagとLayer

Effect<A, E, R>R が本領を発揮するのがここだ。Effect の DI はランタイムコンテナではなく、型で表される環境である。

import { Context, Effect, Layer } from "effect"

// 1. Tag でサービスを宣言
class DbService extends Context.Tag("DbService")<
  DbService,
  {
    readonly getUser: (id: string) => Effect.Effect<User, UserNotFound>
    readonly saveReceipt: (r: Charge) => Effect.Effect<void, DbError>
  }
>() {}

// 2. 使う — yield* で Tag を取り出す
const fetchUser = (id: string) =>
  Effect.gen(function* () {
    const db = yield* DbService
    return yield* db.getUser(id)
  })
// 推論: Effect<User, UserNotFound, DbService>

DbService を呼び出した場所すべてで、RDbService が自動で加わる。コンパイラが「この effect の実行には DbService が環境に必要」を記録する。

Layer — 環境を組み立てる

Layer はサービスの実装を環境に束ねる。

const DbLive = Layer.succeed(DbService, {
  getUser: (id) => /* 実実装 */,
  saveReceipt: (c) => /* 実実装 */,
})

const DbTest = Layer.succeed(DbService, {
  getUser: (id) => Effect.succeed(testUser),
  saveReceipt: () => Effect.void,
})

const program = fetchUser("u1").pipe(Effect.provide(DbLive))
//                                                  ^
//                              テストでは DbTest を差し替える

複数の Layer は Layer.merge / Layer.provide で合成。サービス間依存も型で捕捉される — DbServiceConfig を必要とするなら、Config を差し込まない Layer はコンパイルエラーだ。

これは DI コンテナではない。ランタイムマジックはない — 関数と環境クロージャの変形にすぎないが、型が環境を運ぶので、コンテナの診断価値だけを得て動的ディスパッチのコストを払わない。


5章 · リソース管理 — Scope, acquireRelease, using

リーク発生のパターンは見慣れている。ファイルを開き、途中で例外が出て、close が回らない。

// Promise — リークが起きやすい
async function processFile() {
  const f = await openFile(path)
  const data = await parse(f) // 失敗したら?
  await closeFile(f)          // 届かない
}

finally でしのげるが、複数リソースが絡み、並行性が入ると手が足りなくなる。Effect は Scope という抽象でこれを解く。

acquireRelease

const file = Effect.acquireRelease(
  openFile(path),                         // 取得
  (f) => Effect.promise(() => f.close())  // 解放(必ず実行)
)

const program = Effect.gen(function* () {
  const f = yield* Effect.scoped(file) // scope 内でライフサイクル管理
  return yield* parse(f)
})

Effect.scoped はブロックを作る。ブロックを抜けるとき — 成功・失敗・中断のいずれの経路でも — release が必ず走る。複数のリソースを同じ scope に登録すれば逆順で安全に解かれる。

using — TC39 明示的リソース管理との接続

ECMAScript の using 宣言(Stage 3)は同じ発想を言語レベルへ持ち込んだ。Effect は既にその意味論をライブラリで一貫実装してきた — using が普及すれば自然に統合される。

リソース安全性は分散システムにおけるリーク原因の第1位だ。Effect はここで光る。


6章 · Effect Schema — ランタイムとコンパイル時を一つの定義で

TypeScript の限界: コンパイル時の安全性はランタイムデータ(JSON、env、クエリパラメータ)まで届かない。zod がその椅子を占めてきたが、2026年には強力な対抗馬がいる — Effect Schema (effect/Schema)。

import { Schema as S } from "effect"

const User = S.Struct({
  id: S.String,
  email: S.String.pipe(S.pattern(/^[^@]+@[^@]+$/)),
  age: S.Number.pipe(S.between(0, 150)),
  role: S.Literal("admin", "user", "guest"),
})

type User = S.Schema.Type<typeof User>   // 型推論

const decoded = S.decodeUnknownEither(User)(jsonBlob)
//   ^ Either<User, ParseError>

zod との違い:

  • 双方向(Bidirectional) — encode と decode が一つの定義から同時に出る。Date のような非 JSON 型を自然に扱う。
  • Effect 連携S.decode(schema)(data)Effect<A, ParseError> を返す。検証を他の effect と素直に合成できる。
  • 変換(Transformations)S.transform で「文字列を Date に」「trim+小文字化」のような変換をスキーマ自体に組み込む。
  • Branded typesS.brand で名目型を作る。同じ string 上の UserIdOrderId を区別。

変換の一例

const ISODate = S.Date.pipe(S.transform(
  S.String,
  { decode: (s) => new Date(s), encode: (d) => d.toISOString() }
))

API 境界で一度だけ検証/変換し、内部では正確な型で扱う、という書き方が容易になる。


7章 · Effect Stream — pushとpullを一つのモデルで

大量データ・バックプレッシャー・無限ストリーム — Promise/AsyncIterator では扱いにくい場所。Effect Stream はプル基盤だが push も自然に取り込む。

import { Stream } from "effect"

const numbers = Stream.range(1, 1_000_000)

const program = numbers.pipe(
  Stream.map((n) => n * 2),
  Stream.filter((n) => n % 3 === 0),
  Stream.take(100),
  Stream.run(Sink.collectAll),
)

Stream は遅延だ。末端の Stream.run が流れを起こす。中間変換はバックプレッシャーで自然に絞られる — 下流が捌けなければ上流が止まる。

よくあるパターン

  • Stream.fromAsyncIterable — AsyncIterator をそのまま吸収。
  • Stream.fromQueue — push ソース(webhook、イベント)。
  • Stream.throttle — 周期制御。
  • Stream.broadcast — 1ソースを N コンシューマへ。
  • Stream.fromSchedule — 時間ベースのトリガ。

Node Streams のバックプレッシャーが難しいのは、誰が止まるかが暗黙だからだ。Effect Stream はその決定を型と意味論でライブラリに刻んでいる。


8章 · Fiberと構造化並行性

Effect の並行性プリミティブは Fiber だ。軽量、協調的、キャンセルが伝播する。Promise/Worker ではなく、グリーンスレッドに近い。

const a = Effect.delay(longFetch(...), "100 millis")
const b = longCompute(...)

// 両方同時実行、両方完了で合流
const both = Effect.all([a, b], { concurrency: 2 })

// 先に終わる方を採用、負けた Fiber は自動キャンセル
const fastest = Effect.race(a, b)

構造化並行性が意味すること

要点: 子の寿命は親を超えないrace で a が勝てば b は即座に中断される。acquireRelease で取った資源は中断時点で解放される。親 Fiber が死ねば子 Fiber もすべて死ぬ。「どこかでバックグラウンド作業が生き延びて資源を握り続ける」が起きない。

キャンセル伝播

const stoppable = Effect.gen(function* () {
  yield* Effect.log("作業開始")
  yield* Effect.sleep("10 seconds")
  yield* Effect.log("完了 — この行は中断時には絶対に出ない")
})

const fiber = yield* Effect.fork(stoppable)
yield* Effect.sleep("1 second")
yield* Fiber.interrupt(fiber)  // 即座にキャンセル、後始末も保証

Promise の世界ではキャンセルは一等市民ではない — AbortSignal で部分的にしか取り扱えず、ライブラリごとに対応がまちまちだ。Effect ではすべての effect が中断可能で、Scope が後始末を保証する。


9章 · 相互運用 — Promise・コールバック・AbortSignalと共存する

現実のコードは Promise・コールバック・SDK と混ざる。Effect はその境界を綺麗に扱う。

Promise → Effect

const fetchUser = Effect.tryPromise({
  try: () => fetch(`/api/user/${id}`).then(r => r.json()),
  catch: (e) => new FetchError({ cause: e }),
})

Effect.promise は絶対に失敗しない Promise 用、Effect.tryPromise は失敗をドメインエラーへマッピングする。

コールバック → Effect

const readFile = (path: string) =>
  Effect.async<Buffer, NodeJS.ErrnoException>((resume) => {
    fs.readFile(path, (err, data) => {
      if (err) resume(Effect.fail(err))
      else resume(Effect.succeed(data))
    })
  })

Effect.async は単発コールバック用、Effect.asyncEffect は cleanup を必要とするコールバック用。

AbortSignal ブリッジ

const withAbort = (signal: AbortSignal) =>
  Effect.async<Data, FetchError>((resume) => {
    const c = new AbortController()
    signal.addEventListener("abort", () => c.abort())
    fetch(url, { signal: c.signal })
      .then(r => resume(Effect.succeed(r)))
      .catch(e => resume(Effect.fail(new FetchError({ cause: e }))))
  })

Effect → Promise は Effect.runPromise 一発で済む。つまりEffect は決して孤島ではない — 境界は Promise/コールバック世界と橋を架け、内側だけ Effect で保つ。


10章 · fp-tsからEffect-TSへ — 合流の意味

Effect-TS の歴史は TypeScript 関数型陣営の小さな事件だ。

  • 2017〜 fp-ts (Giulio Canti) — Haskell/PureScript の型クラスを TypeScript に移植。ADT、モナド、レンズまで。学術的だが愛された。
  • 2020〜 Effect-TS (Michael Arnaldi ら) — ZIO の影響を強く受けた新アプローチ。単一 Effect 型中心、最初から production を志向。
  • 2023 Giulio Canti が Effect-TS チームに合流。fp-ts は事実上 maintenance mode へ。Effect-TS が後継として定着。
  • 2024 Effect 3.0。API 安定。パッケージ構成が effect 単一パッケージに集約。
  • 2026 3.x 安定。Schema・Stream・Match・Worker・CLI の周辺パッケージが成熟。

fp-ts が壊れたわけではない — 動く。だが新規コードであれば Effect-TS が明らかな選択だ。同じ人々がそちらで働き、ドキュメント、ツール、導入事例、エコシステムがそちらに集まっている。

実際の導入例

  • Bun — ツールチェーン内部の一部で Effect を採用(公開コードで確認可能)。
  • Disney StreamingVercel チームの一部、その他多数 — カンファレンス発表で Effect の本番導入を共有。
  • 多数の OSS — zod 代替としての Schema、Effect 上に作られたバックエンドフレームワーク。

規模ある TypeScript バックエンドで「管理可能な複雑度」が重要になると、Effect はよく登場する。


11章 · 比較 — Promise・Result・ZIO・Rust Result

Effect だけが答えではない。同じ椅子に別の道具がある。

Plain Promise

  • 長所: 標準、皆知っている、小さなコードでは無理がない。
  • 短所: エラー型が unknown、環境/キャンセルが一等市民でない。
  • : 単一関数・単一モジュール・短いスクリプト・ライブラリ境界。

Result/Either (neverthrow, ts-belt, fp-ts Either)

  • 長所: エラー型を捕まえる、軽量。
  • 短所: 並行性・キャンセル・DI は別途必要。
  • : エラーだけ明示的に追いたいとき。Effect 導入の踏み台にも。

ZIO (Scala)

  • 長所: Effect-TS の精神的兄弟、より長い歴史。
  • 言語差: Scala のみ。TypeScript では Effect が事実上の ZIO。

Rust Result + lifetimes

  • 長所: エラー・リソース・所有権をコンパイラが丸ごと強制。
  • 言語差: 別パラダイム。JS インフラとは距離。

F# コンピュテーション式 / Haskell IO

  • 長所: より深い型システムと一貫したエフェクト追跡。
  • 言語差: 同じ椅子、別の部族。Effect-TS はその精神を TS に移したもの。

ひと言まとめ

シナリオおすすめ
短いスクリプト・単一モジュールPlain Promise
型付きエラーだけ欲しいResult ライブラリ
大規模バックエンド、リファクタ安全性・DI・並行性の一貫性Effect-TS
Scala エコシステムZIO
システムプログラミングRust Result + lifetimes

12章 · Effectが正解な場面と過剰な場面

Effect はただではない。学習コスト、ランタイムオーバーヘッド、チーム合意 — みなコストだ。

Effect が向く場面

  • バックエンドが中規模以上、寿命が数年単位。
  • リファクタ安全性がビジネス価値(決済、整合性、IAP)。
  • テスト容易性が重要 — DI/Layer がユニットテストを単純化する。
  • 資源管理(ファイル、コネクション、ロック)が複雑。
  • 並行性・キャンセルが頻出する。
  • チームが関数型/エフェクトパラダイムに慣れているか、学ぶ意志がある。

過剰な場面

  • CLI 1個簡単なスクリプトプロトタイプ
  • チームが新人ばかりで、ドメインも不安定な小規模スタートアップ。
  • React コンポーネント内部だけで使う UI ロジック(UI 境界には適だが全面導入は重い)。
  • ライブラリ作者 — ユーザに Effect 依存を強要しにくい。

シグナル: 「コードが try/catch とクロージャ依存のキルトになり、リファクタひとつで一週間吹き飛ぶ」 — Effect の出番。「200行のスクリプト、cron で 1 分に 1 回」 — async/await で十分。


13章 · 導入戦略 — 一気にやらない

Effect 導入で最もよくある失敗: すべてのコードを一気に置き換えようとする。結果、1 万行の PR ができ、レビューが詰まり、チームが怒る。

段階的導入

  1. 境界から — 1モジュール/サービスで開始。外部境界は Promise を保ち、内側だけ Effect。
  2. Schema から — DI/Stream を入れずに Effect Schema だけ導入しても検証コードが綺麗になる。
  3. Result 段階 — まず Either/Result でエラーだけ捕まえ、慣れたら Effect へ。
  4. Layer 導入 — コアサービスから Layer に抽出。テストダブルが自然に出てくる。
  5. 新規コードだけ — 既存はそのまま、新モジュールは Effect で。

チーム学習

  • 最初の2週: Effect 型を読むことに慣れる。
  • 次の2週: Effect.gen と基本演算。
  • その次: エラー区分、Layer、Scope。
  • 最後: Stream、Fiber。

関数型ライブラリは最初は誰でも「魔法」と感じる。Effect は読みやすい形でその魔法を減らしているが、それでもパラダイムシフトだ。


14章 · 短いミニ例 — 決済パイプライン

冒頭の決済コードを Effect で書き直す。

import { Effect, Layer, Data, Context, Schema as S } from "effect"

class UserNotFound extends Data.TaggedError("UserNotFound")<{ id: string }> {}
class PaymentDeclined extends Data.TaggedError("PaymentDeclined")<{ code: number }> {}

class DbService extends Context.Tag("DbService")<DbService, {
  readonly getUser: (id: string) => Effect.Effect<User, UserNotFound>
  readonly saveReceipt: (c: Charge) => Effect.Effect<void>
}>() {}

class StripeService extends Context.Tag("StripeService")<StripeService, {
  readonly getDefaultCard: (id: string) => Effect.Effect<Card>
  readonly charge: (card: Card, amt: number) => Effect.Effect<Charge, PaymentDeclined>
}>() {}

class MailerService extends Context.Tag("MailerService")<MailerService, {
  readonly sendReceipt: (to: string, c: Charge) => Effect.Effect<void>
}>() {}

const chargeCustomer = (userId: string, amount: number) =>
  Effect.gen(function* () {
    const db = yield* DbService
    const stripe = yield* StripeService
    const mailer = yield* MailerService

    const user = yield* db.getUser(userId)
    const card = yield* stripe.getDefaultCard(user.stripeId)
    const charge = yield* stripe.charge(card, amount)
    yield* db.saveReceipt(charge)
    yield* mailer.sendReceipt(user.email, charge)
    return toReceipt(charge)
  })
// 推論されるシグネチャ:
// Effect<Receipt,
//        UserNotFound | PaymentDeclined,
//        DbService | StripeService | MailerService>

シグネチャだけで読み手は分かる:

  • 何が返るか: Receipt
  • 何が失敗しうるか: UserNotFound または PaymentDeclined — それだけ。
  • 何が必要か: DbServiceStripeServiceMailerService

テストは3サービスのモック Layer を差し込めば終わり。新エラーが増えればコンパイラが光る。新依存が増えればシグネチャが変わる。これがエフェクト追跡の価値だ。


エピローグ — エフェクトは型をより饒舌にする

Effect-TS をひと言で: 型がもっと語る。関数が何を返すかだけでなく、何が失敗しうるか、何が必要か、どうキャンセルされるかがシグネチャに刻まれる。

async/await がコールバックを置き換えたように、Effect は async/await を置き換えるのではない — そのにもっと豊かな意味論を載せる。時間が経てば using のような ECMAScript 標準がその意味論の一部を言語に取り込む。Effect はその道の先頭をライブラリでいち早く見せている存在だ。

大規模バックエンドの5年目に「この関数は何を投げ、何を必要とするか」を知るためにソースを最初から読まないといけない状態 — そこが Effect の出番だ。小さなスクリプトなら — 必要ない。

12項目チェックリスト

  1. ドメインエラーを Data.TaggedError で定義したか?
  2. 関数シグネチャにエラーユニオンが正直に出ているか?
  3. try/catch でドメイン失敗と defect を混ぜていないか?
  4. サービスを Context.Tag + Layer で分けたか?
  5. テスト用 Layer と本番用 Layer は分かれているか?
  6. 資源取得は acquireRelease/scoped で包んでいるか?
  7. 並行処理に Effect.all の並行度オプションを設定したか?
  8. Effect.race の後、敗者の後始末を確認したか?
  9. 外部データを Effect Schema で一度だけ検証しているか?
  10. Stream は Stream.run まで遅延を保っているか?
  11. Promise/コールバック境界で tryPromise/async を使っているか?
  12. 導入戦略は段階的か — 一気にやろうとしていないか?

アンチパターン10

  1. ドメイン失敗を Error 一種類にまとめる。
  2. try/catch で defect まで呑み込み、本物のバグを隠す。
  3. DI なしでモジュールトップにクライアントを import — テスト不能。
  4. acquireRelease なしにファイル/コネクションを開く。
  5. Effect.all の並行度上限を抜かして N×リクエスト爆発。
  6. Effect.race の敗者後始末が要らないと思い込む。
  7. Stream を遅延せず即実行で使う。
  8. 大コードベースを 1 PR で一気に置き換えようとする。
  9. ライブラリ作者がユーザに Effect 依存を強要する。
  10. 200行の CLI に Effect を入れて学習コストを正当化する。

次回予告

候補: Effect Schema 徹底解説 — zod との差、変換、ブランド型Effect Stream でバックプレッシャー・パイプラインを組むEffect Layer でマイクロサービスを立ち上げる — config・logging・tracing・observability

「型がもっと語れば、リファクタは怖くなくなる。Effect はその約束だ。」

— Effect-TS 徹底解説、了。


参考 / References