blog.dopana

Back

Effect-TS là thư viện functional cho TypeScript — giải quyết 3 vấn đề: error handling type-safe, dependency injection, và effect composition.

import { Effect } from 'effect'

// Effect<Success, Error, Requirements>
const program: Effect.Effect<string, Error, never> = Effect.sync(() => {
  return 'Hello, Effect!'
})

Effect.runSync(program) // "Hello, Effect!"
typescript

Không throw, không try/catch mất type. Mọi lỗi đều được type annotations.

Vấn Đề try/catch#

// ❌ TypeScript không biết function này throw gì
async function getUser(id: string) {
  const res = await fetch(`/api/users/${id}`)
  if (!res.ok) throw new Error('API error')
  return res.json()
}

// Dùng:
try {
  const user = await getUser('123')
  // user: any — mất type
} catch (e) {
  // e: unknown — không biết lỗi gì
}
typescript
// ✅ Effect biết chính xác error type
const getUser = (id: string): Effect.Effect<User, ApiError, never> =>
  Effect.tryPromise({
    try: () => fetch(`/api/users/${id}`).then(r => r.json()),
    catch: () => new ApiError('Failed to fetch'),
  })

// Dùng:
const result = Effect.runSync(getUser('123'))
// result: User — type-safe
// Nếu lỗi: compile-time biết là ApiError
typescript

Core Types#

Effect#

// Effect<Success, Error, Requirements>
type Effect<A, E, R>

// A — kiểu trả về khi thành công
// E — kiểu lỗi
// R — dependencies cần (DI)
typescript
// never = không có lỗi / không cần DI
Effect<string, never, never>  // Luôn thành công, không cần DI
Effect<string, Error, never>  // Có thể lỗi, không cần DI
Effect<string, never, Config> // Cần Config service, không lỗi
Effect<string, Error, Config> // Có thể lỗi, cần Config
typescript

Option#

import { Option } from 'effect'

// Option = Some | None — thay thế null/undefined
const maybe = Option.some('hello')  // Option<string>
const none = Option.none()          // Option<never>

Option.match(maybe, {
  onNone: () => 'nothing',
  onSome: (v) => `got ${v}`,
})
typescript

Either#

import { Either } from 'effect'

// Either<Left, Right> — Left = lỗi, Right = thành công
const success: Either.Either<string, number> = Either.right(42)
const failure: Either.Either<string, number> = Either.left('error')
typescript

Building Effects#

Composing Effects#

map / flatMap#

const greet = Effect.sync(() => 'World')
  .pipe(
    Effect.map((name) => `Hello, ${name}!`),
    Effect.map((msg) => msg.toUpperCase()),
  )
// Effect<string, never, never>
typescript
const program = Effect.sync(() => 'user-1')
  .pipe(
    Effect.flatMap((id) => fetchUser(id)),  // Effect<User, Error, never>
    Effect.map((user) => user.name),
  )
typescript

Gen (giống async/await)#

const program = Effect.gen(function* () {
  const user = yield* fetchUser('123')
  const posts = yield* fetchPosts(user.id)
  return { user, posts }
})
// Effect<{ user: User; posts: Post[] }, Error, never>
typescript

Giống async/await nhưng type-safe — biết chính xác error type ở mỗi step.

All (song song)#

const [user, posts, comments] = yield* Effect.all([
  fetchUser('123'),
  fetchPosts('123'),
  fetchComments('123'),
], { concurrency: 3 })
// Chạy song song 3 request
typescript

Error Handling#

catchAll / catchTag#

const program = fetchUser('123').pipe(
  Effect.catchAll((error) =>
    Effect.sync(() => ({ id: 'default', name: 'Guest' }))
  ),
)

// Catch theo tag (discriminated union)
Effect.catchTag(program, 'NotFound', (error) =>
  Effect.sync(() => ({ id: 'guest', name: 'Guest' }))
)
typescript

retry#

const program = fetchUser('123').pipe(
  Effect.retry({
    times: 3,
    delay: (attempt) => 1000 * Math.pow(2, attempt),  // Exponential backoff
  }),
)
typescript

timeout#

const program = fetchUser('123').pipe(
  Effect.timeout('5 seconds'),
  Effect.catchTag('TimeoutException', () =>
    Effect.sync(() => fallbackUser)
  ),
)
typescript

Dependency Injection#

Dependency được xử lý ở compile-time và runtime — không cần decorator, không global state.

Schema#

Effect-Schema cho validation + serialization type-safe:

Fiber (Concurrency)#

import { Effect, Fiber } from 'effect'

// Fork — chạy concurrent
const fiber = yield* Effect.fork(longRunningTask)

// Làm việc khác...
yield* otherWork()

// Join — đợi kết quả
const result = yield* Fiber.join(fiber)

// Hoặc race
const winner = yield* Effect.race(fetchFromApi, fetchFromCache)
typescript

Resource Management#

import { Effect } from 'effect'

// Scope — tự động giải phóng resource
const program = Effect.acquireRelease(
  Effect.sync(() => openFile('data.txt')),     // acquire
  (file) => Effect.sync(() => file.close()),    // release
).pipe(
  Effect.flatMap((file) => file.read()),
)
// Dù thành công hay lỗi → release luôn chạy
typescript

Practical Example#

Khi Nào Dùng Effect-TS?#

✅ Nên dùng#

  • Backend service — error handling + DI type-safe
  • Data pipeline — nhiều step, cần retry/timeout
  • Domain-driven design — typed errors là first-class
  • Large TypeScript codebase — cần kiểm soát error flow

❌ Không nên#

  • Small project — boilerplate không đáng
  • Frontend app đơn giản — React Query xử lý được
  • Team không quen FP — learning curve cao
  • Quick prototype — throw + try/catch nhanh hơn

Tổng Kết#

Effect-TS biến error handling từ runtime thành compile-time concern.

// ❌ Try/catch
function divide(a: number, b: number): number {
  if (b === 0) throw new DivideByZeroError()
  return a / b
}
// TypeScript: number — nhưng thực tế có thể throw

// ✅ Effect
function divide(a: number, b: number): Effect.Effect<number, DivideByZeroError, never> {
  return b === 0
    ? Effect.fail(new DivideByZeroError())
    : Effect.sync(() => a / b)
}
// TypeScript: Effect<number, DivideByZeroError, never> — chính xác
typescript
try/catchEffect
Error typeunknownType-safe
CompositionTry/catch lồng nhau.pipe, flatMap, gen
RetryTự viết.retry()
TimeoutPromise.race.timeout()
DIConstructor/DI containerContext.Tag
ConcurrencyPromise.allEffect.all, Fiber
Resourcetry/finallyacquireRelease
SchemaZod/zod validationSchema + Effect

Effect-TS không phải cho mọi project. Cho backend TypeScript phức tạp, nó thay đổi cách bạn nghĩ về error handling.

Tài liệu tham khảo#