はじめに
TypeScriptでアプリケーションを開発していると、エラーハンドリングや非同期処理の管理が複雑になってきますよね。try-catchのネストが深くなったり、Promiseチェーンが読みづらくなったり。個人的には、こういう「動くけど保守が辛い」コードが一番厄介だと思っています。
そんな悩みを解決してくれるのが Effect というライブラリです。GitHubスター数は12,000を超えて、最近かなり注目を集めています。
Effectは、TypeScriptで堅牢なアプリケーションを構築するためのエコシステム。エラーハンドリング、非同期処理、依存性注入などを型安全に扱えるようになります。関数型プログラミングの知識がなくても、Promiseを使うような感覚で始められるのが特徴ですね。
特徴・メリット
型安全なエラーハンドリング
Effectの最大の特徴は、エラーを「値」として扱える点です。従来のtry-catchでは、どんなエラーが発生するか型レベルでは分からなかった。Effectでは、発生しうるエラーが型として明示されます。
// Effect<成功の型, エラーの型, 依存関係の型>
Effect<User, NotFoundError | NetworkError, ApiClient>
これ、意外と重要で。型定義を見ただけで「この処理は何が失敗する可能性があるのか」が分かるんですよね。
非同期処理の簡潔な記述
複雑な非同期処理も、パイプラインで直感的に書けます。リトライ、タイムアウト、並行処理といった機能が標準で用意されていて、それらを組み合わせるだけ。
組み込みのトレーシング
OpenTelemetryとの統合が組み込まれているので、パフォーマンス計測やデバッグが楽になります。本番環境での問題解析に役立つポイントですね。
軽量なランタイム
圧縮・ツリーシェイク後で約15kBと軽量。バンドルサイズを気にする方も安心です。
幅広いプラットフォーム対応
Node.js、Deno、Bun、Cloudflare Workers、ブラウザなど、様々な環境で動作します。React、Next.js、Viteといったフレームワークとも問題なく使えます。
インストール方法
npmやpnpmでインストールできます。
# npm
npm install effect
# pnpm
pnpm install effect
# yarn
yarn add effect
基本的な使い方
Effect型の基本
Effectの中心となるのは Effect<A, E, R> という型です。
- A(Success): 成功時に返される値の型
- E(Error): 発生する可能性のあるエラーの型
- R(Requirements): 実行に必要な依存関係の型
import { Effect } from "effect"
// 成功するEffectを作成
const success = Effect.succeed(42)
// 失敗するEffectを作成
const failure = Effect.fail(new Error("Something went wrong"))
重要なのは、Effectは「遅延実行」されるという点。作成した時点では何も実行されず、明示的に実行するまで待機します。
同期処理のラップ
import { Effect } from "effect"
// 失敗しない同期処理
const log = (message: string) =>
Effect.sync(() => console.log(message))
// 失敗する可能性のある同期処理
const parseJson = (input: string) =>
Effect.try({
try: () => JSON.parse(input),
catch: (error) => new Error(`JSON parse failed: ${error}`)
})
非同期処理のラップ
import { Effect } from "effect"
// 失敗しない非同期処理
const delay = (ms: number) =>
Effect.promise(() =>
new Promise(resolve => setTimeout(resolve, ms))
)
// 失敗する可能性のある非同期処理
const fetchTodo = (id: number) =>
Effect.tryPromise({
try: () => fetch(`https://jsonplaceholder.typicode.com/todos/${id}`),
catch: () => new Error("Network request failed")
})
パイプラインの構築
Effectの真価は、処理をパイプラインで組み合わせるところにあります。
import { Effect, pipe } from "effect"
const program = pipe(
fetchTodo(1),
Effect.andThen((response) => response.json()),
Effect.andThen((data) => {
console.log("Fetched:", data.title)
return data
}),
Effect.catchAll((error) => {
console.error("Error:", error.message)
return Effect.succeed({ title: "Fallback" })
})
)
// 実行
Effect.runPromise(program)
Effectの実行
作成したEffectを実行するには、いくつかの方法があります。
import { Effect } from "effect"
const program = Effect.succeed(42)
// 同期的に実行(失敗時は例外をスロー)
const result1 = Effect.runSync(program) // 42
// Promiseとして実行
const result2 = await Effect.runPromise(program) // 42
// Exit型で結果を取得(成功/失敗を明示的に扱う)
const exit = Effect.runSyncExit(program)
// { _id: "Exit", _tag: "Success", value: 42 }
実践的なユースケース
APIクライアントの実装
実際のプロジェクトでよく使うパターンを紹介します。
import { Effect, pipe } from "effect"
// カスタムエラー型の定義
class NetworkError {
readonly _tag = "NetworkError"
constructor(readonly message: string) {}
}
class ValidationError {
readonly _tag = "ValidationError"
constructor(readonly field: string, readonly message: string) {}
}
// APIクライアント
const fetchUser = (id: number) =>
pipe(
Effect.tryPromise({
try: () => fetch(`/api/users/${id}`),
catch: () => new NetworkError("Failed to fetch user")
}),
Effect.andThen((response) => {
if (!response.ok) {
return Effect.fail(new NetworkError(`HTTP ${response.status}`))
}
return Effect.tryPromise({
try: () => response.json(),
catch: () => new NetworkError("Failed to parse response")
})
}),
Effect.andThen((data) => {
if (!data.email) {
return Effect.fail(new ValidationError("email", "Email is required"))
}
return Effect.succeed(data)
})
)
// エラーの種類に応じた処理
const program = pipe(
fetchUser(1),
Effect.catchTag("NetworkError", (error) => {
console.error("Network issue:", error.message)
return Effect.succeed({ name: "Guest", email: "" })
}),
Effect.catchTag("ValidationError", (error) => {
console.error(`Validation failed: ${error.field} - ${error.message}`)
return Effect.succeed({ name: "Guest", email: "" })
})
)
リトライとタイムアウト
import { Effect, Schedule, Duration } from "effect"
const fetchWithRetry = pipe(
fetchTodo(1),
// 3回までリトライ(指数バックオフ)
Effect.retry(Schedule.exponential(Duration.seconds(1)).pipe(
Schedule.compose(Schedule.recurs(3))
)),
// 10秒でタイムアウト
Effect.timeout(Duration.seconds(10))
)
並行処理
import { Effect } from "effect"
// 複数のEffectを並行実行
const fetchMultiple = Effect.all([
fetchTodo(1),
fetchTodo(2),
fetchTodo(3)
], { concurrency: "unbounded" })
// 結果は配列で返される
まとめ
Effectは、TypeScriptで堅牢なアプリケーションを構築するための強力なツールです。
正直なところ、最初は学習コストがあります。ただ、公式ドキュメントにも書いてある通り「いくつかの関数を覚えるだけで、生産性の80%を享受できる」というのは本当だと思います。
個人的には、以下のような場面で特に効果を発揮すると感じています。
- エラーハンドリングが複雑になりがちなAPIクライアント
- 複数の非同期処理を組み合わせるビジネスロジック
- 型安全性を重視するチーム開発
30代になって思うのは、「動けばいい」から「保守しやすい」コードへの意識が重要になってきたということ。Effectは、その観点でかなり優秀な選択肢だと思います。
まずは小さなプロジェクトで試してみて、手に馴染んできたら本格的に導入を検討する、という流れがおすすめですね。
