2518 文字
13 分
令和のこの時代にバックエンド TypeScript をやるなら Effect-TS を使え

5月に​入り,​ついに​ Drizzle ORM v1.0.0-rc.1 が​リリースされ,​なんと​ Effect v4 の​ネイティブサポートが​入った.

これを​見て​私は​ 2026年は​ Effect-TS 元年であると​確信した.​今まで​ Effect-TS 側が​何か​ライブラリや​ランタイムに​向けた​アダプターを​サポートする​ことは​あったが,​ライブラリ側から​ Effect-TS に​歩み寄ってくる​ことは​今までに​なかった.​しかも​ Drizzle が.

めっちゃクリックベイトみたいな​タイトルだが,​割と​本当だと​思っている.

巷では​「バックエンド TypeScript は​やめろ」みたいな​話題が​定期的に​飛び交うが,​私は​そうは​思わない.​ライブラリも,​デプロイする​環境も​数多く​ある.​動作速度も​必要十分である.

そして​何より,​フロントエンドでは​ TypeScript が​デファクトスタンダードと​なっている​現状で,​あえて​バックエンド用に​新たな​言語を​習得する​コストが​高いと​考えている​ためである.

そんな​筋金入りの​バックエンド TypeScript 推進派の​私だが,​もちろん否定派の​意見にも​理が​ある​部分は​ある.​ 否定派の​言い分は​おおむね以下の​3点に​集約される​:

  • ライブラリが​頻繁に​壊れる​・塩漬けビリティが​低い
  • Rails のように​思想を​持って開発を​導いてくれる​骨太な​フレームワークが​ NestJS くらいしかなく,しかも​それは​控えめに​言って​使えた​ものではない
  • ORM ねーじゃん

これらの​問題は​ Effect-TS を​使えば​解決するのではないか,​そう​考えている.

Effect-TS とは​何者

Effect-TS は​ TypeScript 上で​動く​エフェクトシステム,​関数型コンビネータ,​標準ライブラリ群である.

エフェクトシステムと​いう​意味​不明な​単語が​突然登場したが​,​ざっくり説明すると,​失敗するかもしれない​処理や​非同期処理,​副作用を​伴う​処理や​依存に​よって​実現する​処理を​1つの​ Effect<A, E, R>と​いう​型で​一括で​扱う​ Haskell 由来の​仕組みである.​Effect-TS で​書かれた​コードの​中には​ Promiseも​ async/awaitも​登場しない.Aが​成功時の​値,Eが​失敗時の​エラー,Rが​ Effect 自体の​実行に​必要な​依存を​それぞれ表している.

import { Effect } from "effect"

// User を返し,NotFoundError で失敗する.実行には UserRepo が必要.
const findUser = (id: number): Effect.Effect<User, NotFoundError, UserRepo> =>
  Effect.gen(function* () {
    const repo = yield* UserRepo // R = UserRepo
    const user = yield* repo.findById(id) // (id: number) => Effect<User, NotFoundError>
    return user // User
  })

これだけ​見れば​「ふーん,​ただの​ Result型ね」で​終わってしまうかもしれないが,​この​ Effect型を​中心に​エコシステム全体が​同じ​思想で​組み​上がっているのが​ Effect-TS の​真骨頂である.

不満に​反論する#

ライブラリが​壊れる#

Effect の​エコシステムに​閉じれば​かなり​緩和される.@effect/*と​いう​名前​空間で​公式が​用意している​標準ライブラリが​利用可能である.argon2など,​特殊な​処理を​する​ライブラリを​入れない​限りは​ほぼ Effect エコシステム内で​バックエンドを​構築できる.

  • effect/Schema: バリデーション・パース・シリアライズ
  • @effect/platform: HTTP サーバー・​クライアント,​ファイルシステム,​KVS
  • @effect/sql: SQL クライアント,​クエリビルダ,​簡易マイグレータ
  • @effect/cli: CLI ツール
  • @effect/ai: LLM クライアントの​抽象化

これらが​すべて​ Effect 本体と​同じ​思想・​同じリリースサイクルで​管理されている.​バリデーションは​ Zod, HTTP サーバーは​ Hono, DB は​ Drizzle, ロガーは​ Pino…みたいな​組み合わせを​やらなくても​良い.

無論,​頼る​ライブラリセットを​絞れば,​ライブラリが​壊れると​いう​リスクは​小さくなる.また,​Effect 本体の​ API は​現在 v4 の​リリースが​計画されており,​現在 beta と​なっている.​これが​ LTS と​なる​予定で​あり,​今後​メジャーバージョンを​頻繁に​出さない​方​針が​明言されている1.​ v3 もすでに​ feature freeze 済みで,​v4 安定後も​保守は​継続される​予定だ.

思想の​ない​フレームワーク#

これに​関しては​ Effect-TS こそが​思想​その​ものである.

Effect-TS は​ライブラリである​一方,​フレームワークでもある.

  • 依存性注入: Context/ Layer
  • 制御フロー: Effect.gen
  • エラーハンドリング: Effect.catchTag/ Effect.catchAll
  • リソース管理: Effect.acquireRelease
  • 並行制御: Effect.fork/ Effect.race/ Effect.all({ concurrency })
  • HTTP サーバ: @effect/platform/HttpApi
import { HttpApi, HttpApiEndpoint, HttpApiGroup } from "@effect/platform"
import { Schema } from "effect"

const User = Schema.Struct({
  id: Schema.Number,
  name: Schema.String,
})

const UsersApi = HttpApiGroup.make("users")
  .add(
    HttpApiEndpoint.get("getById", "/users/:id")
      .setPath(Schema.Struct({ id: Schema.NumberFromString }))
      .addSuccess(User)
      .addError(Schema.TaggedStruct("NotFound", {}), { status: 404 })
  )
  .add(
    HttpApiEndpoint.post("create", "/users")
      .setPayload(Schema.Struct({ name: Schema.String }))
      .addSuccess(User)
  )

const Api = HttpApi.make("api").add(UsersApi)

あとは​この​ Apiに​対して​ HttpApiBuilderで​ハンドラを​実装するだけで,​リクエストの​パース・バリデーション・レスポンスの​シリアライズ・エラーレスポンスが​全部​型に​乗ったまま​動く.

Rails ほど​ Convention over Configuration を​押し付けてくるわけではないが,​少なくとも​「ルーティングは​これ,​DI は​これ,​バリデーションは​これ」と​ある​程度バックエンドサーバーを​実装する​上での​フローが​ Effect エコシステム上に​構築されている.

ORM ねーじゃん#

Effect には​長らく,​薄い​ SQL クライアントしか​存在しなかった​(@effect/sql).だが,​冒頭でも​触れた​とおり,​Drizzle に​ Effect の​ネイティブサポートが​追加された.

import { PgClient } from "@effect/sql-pg"
import * as PgDrizzle from "drizzle-orm/effect-postgres"
import { Effect, Redacted } from "effect"
import { usersTable } from "./schema"
import { relations } from "./relations"

const PgClientLive = PgClient.layer({
  url: Redacted.make(process.env.DATABASE_URL!),
})

const DB = PgDrizzle.make({ relations }).pipe(
  Effect.provide(PgDrizzle.DefaultServices)
)

const program = Effect.gen(function* () {
  const db = yield* DB
  const users = yield* db.select().from(usersTable)
  return users
}).pipe(Effect.provide(PgClientLive))

await Effect.runPromise(program)

Effect.genの​中で​ Drizzle の​ db.select()を​ Effect.tryPromiseで​包む必要は​もうない.yield*するだけで​ SELECT 文の​戻り値が​左辺に​束縛される.​コネクションも​ PgClientを​通じて​提供される.

従来は​ Drizzle の​ Promise ベース API と​ Effect の​世界との​あいだに​グルーコードが​必要だったが,​それが​消えた.


と​いう​話を​最近​ VRChat の​技術コミュニティ内の​至る​所で​行っており,​「隙あらば Effect 語り」,​隙E語を​していたのだが,​その​中で​こんな​声も​聞いた.

関数型プログラミングを​理解していないと​辛い?#

これは​半分 Yes で,​半分 No かなと​思う.​Effect の​根っこの​部分には​確かに​関数型的な​発想が​強く​入っているが,​書き手と​して​それらを​理解する​必要は​必ずしも​ない.

Effect.genを​使うと,​手続き型的な​コードに​変化させられる.yield*は​ awaitの​代わりに​なるし,​そう​考えると​ Effect.genは​ asyncとも​取る​ことができる.awaitと​違う​点は,​エラーも​依存も​型に​載ると​いう​点ぐらいではないだろうか.

関数型の​エッセンスは​あったら​便利だが,​別に​無くても​書けると​いう​ところが​ Effect-TS を​ Effect-TS たらしめている​大きな​要素だと​考えているし,​それを​体現する​ためには​他の​静的型付け言語ではなく,​TypeScript である​必要が​あったのだと​考えている.

全部​ pipeで​書いてやる​ぜヒャッハー!​な​人は​そうすれば​良いし,Effect.genで​手続き的に​書きたいぜ,って​人も​そうすれば​良い.

ボイラープレートが​多くなる?#

間違いなく​これは​ Yes である.​明日から​ async () => {}を​ Effect.gen(function* () {})で​書いてください​なんて​言われれば​正直気が​狂う.​コードベースも​全体​的に​ Promiseと​クラスで​サクッと​書くのに​比べれば​記述が​増えるのは​事実である.

ところが,​これは​果たして​ 2026 年に​おいて​欠点と​言えるだろうか,と​いうのを​問いたい.

今​日​日,​アプリケーションコードの​ほとんどは​ AI エージェントに​生成させる​時代である.​ある​程度 Effect で​書かれた​コードベースを​一度​発見すれば,​AI エージェントは​それを​模倣し,​実装を​行ってくれる.​人間は​コードを​書く​ことよりも,​書かれた​コードを​読んで​検証し,​方​針を​定める​ことが​中心に​なりつつある.

よって,​書く​コストは​ AI に​ほぼ吸収され,​読むコスト・実装の​誤りに​気づき,​修正する​コストだけを​人間が​負うことに​なる.​この​コストを​さらに​最小化させる​ために​ Effect を​活用できないかと​いう​ところである.

Effect<User, NotFoundError | DbError, UserRepo | Logger>と​いう​1つの​型シグネチャを​読めば,​この​処理が​「何を​返すのか」​「何で​失敗しうるのか」​「何に​依存しているのか」が​すべて​分かる.​LSP で​カーソルを​当てれば​そのまま​ホバーに​出る.

Effect.catchTagで​ NotFoundErrorを​ハンドルし忘れていれば,​ハンドルを​忘れた​エラー型が​そのまま​ Eに​乗り続け,​浮かび​上がってくる.​依存を​提供し忘れていれば,Rが​ neverにならず Effect を​実行する​場所で​型エラーが​発生する.

つまり,​AI が​書いた​コードを​人間が​読むとき,​その​妥当性を​型と​ LSP が​即座に​フィードバックしてくれる.​これは​「AI が​書く​ → LSP を​通じて​ AI に​型エラーが​伝えられる​ → 修正の​ために​また​ AI が​書く」と​いう​実装の​ループを​人間が​関わる​ことなく​組み立てる​ことができると​いう​ことである.

ボイラープレートが​多少増えても,​この​開発速度と​安定性を​得られるのなら,​それに​越した​ことはないと​考える.​ボイラープレートの​コストは​ AI に​押し付けて,​型​安全性の​リターンだけ​人間が​受け取る​ことができる​時代が​やってきたと​いう​ことである.

まとめ#

と​いうわけで,​TypeScript を​バックエンドの​言語と​して​選定しようと​している​ときには,​是非一度​検討してみて​ほしい.

Footnotes#

  1. https://effect.website/blog/releases/effect/40-beta/

GitHubで編集を提案