Home > Blog > tech

Effect-TS คืออะไร? Functional Programming สำหรับ TypeScript ที่จัดการ Side Effect ได้ดี 2026

effect ts functional programming guide
Effect-TS Functional Programming Guide 2026
2026-04-11 | tech | 3500 words

Effect-TS เป็น Library สำหรับ TypeScript ที่เปลี่ยนวิธีคิดเรื่องการจัดการ Side Effects, Error Handling และ Dependency Injection อย่างสิ้นเชิง ถ้าคุณเคยเขียน TypeScript แล้วรู้สึกว่าการจัดการ async/await ที่ซ้อนกันหลายชั้น, try-catch ที่จับ Error ไม่ครบ, หรือ Dependency ที่ส่งผ่าน constructor แล้วยุ่งเหยิง Effect-TS คือคำตอบที่คุณตามหา

ในปี 2026 Effect-TS (หรือเรียกสั้นๆ ว่า Effect) กลายเป็นหนึ่งใน Library ที่เติบโตเร็วที่สุดใน TypeScript ecosystem ด้วยแนวคิด Functional Programming ที่นำเรื่อง typed errors, composable computations และ structured concurrency มาใช้อย่างเป็นระบบ

Functional Programming — แนวคิดพื้นฐาน

Pure Functions

Pure Function คือฟังก์ชันที่ให้ผลลัพธ์เดิมเสมอเมื่อ Input เดิม (Deterministic) และไม่มี Side Effect (ไม่เปลี่ยนแปลงสิ่งภายนอก ไม่เรียก API ไม่อ่านไฟล์ ไม่เขียน Database) ข้อดีคือ ทดสอบง่าย, เข้าใจง่าย, compose ได้ และ parallelize ได้

// Pure Function
const add = (a: number, b: number): number => a + b
const double = (n: number): number => n * 2

// Impure Function (มี Side Effect)
let total = 0
const addToTotal = (n: number): void => { total += n } // เปลี่ยน state ภายนอก
const getTime = (): number => Date.now()                 // ผลลัพธ์ต่างกันทุกครั้ง
const fetchUser = (id: string) => fetch(`/users/${id}`) // เรียก network

Immutability

ข้อมูลไม่ถูกเปลี่ยนแปลง (mutate) แต่สร้างใหม่ทุกครั้ง ใน TypeScript ใช้ readonly, as const, Object.freeze() หรือ Library เช่น Immer

// Mutable (ไม่ดี)
const arr = [1, 2, 3]
arr.push(4)  // เปลี่ยน arr โดยตรง

// Immutable (ดี)
const arr2 = [1, 2, 3] as const
const arr3 = [...arr2, 4]  // สร้าง Array ใหม่

// Readonly type
interface User {
  readonly id: string
  readonly name: string
  readonly email: string
}

Composition

การประกอบฟังก์ชันเล็กๆ เข้าด้วยกันเพื่อสร้างฟังก์ชันที่ซับซ้อนขึ้น นี่คือหัวใจของ Functional Programming

// Function composition
const trim = (s: string): string => s.trim()
const toLower = (s: string): string => s.toLowerCase()
const sanitize = (s: string): string => toLower(trim(s))

// ปัญหา: ซ้อนกันหลายชั้นอ่านยาก
const result = validate(parse(sanitize(trim(input))))

// แก้ด้วย pipe (Effect-TS)
import { pipe } from "effect"

const result2 = pipe(
  input,
  trim,
  sanitize,
  parse,
  validate
)

Effect Type — หัวใจของ Effect-TS

Effect<A, E, R> คือ Type หลักของ Effect-TS ซึ่งเป็น description ของ computation ที่ยังไม่ได้ทำงาน (lazy) มี 3 Type parameters คือ A (Success type — ค่าที่ได้เมื่อสำเร็จ), E (Error type — Error ที่อาจเกิดขึ้น), R (Requirements — Dependencies ที่ต้องการ)

import { Effect } from "effect"

// Effect ที่สำเร็จเสมอ (ไม่มี Error, ไม่มี Dependency)
const succeed: Effect.Effect<number> = Effect.succeed(42)

// Effect ที่ fail เสมอ
const fail: Effect.Effect<never, Error> = Effect.fail(new Error("oops"))

// Effect ที่อาจสำเร็จหรือ fail
const divide = (a: number, b: number): Effect.Effect<number, Error> =>
  b === 0
    ? Effect.fail(new Error("Division by zero"))
    : Effect.succeed(a / b)

// Effect จาก async operation
const fetchUser = (id: string): Effect.Effect<User, HttpError> =>
  Effect.tryPromise({
    try: () => fetch(`/api/users/${id}`).then(r => r.json()),
    catch: (error) => new HttpError(String(error))
  })

// สำคัญ: Effect เป็น lazy — ยังไม่ทำงานจนกว่าจะ "run"
// ต่างจาก Promise ที่ทำงานทันทีที่สร้าง
Effect vs Promise: Promise ทำงานทันทีที่สร้าง (eager) และไม่มี typed errors (catch ได้แค่ unknown) Effect เป็น lazy (ไม่ทำงานจนกว่าจะ run), มี typed errors (รู้ว่า Error ชนิดไหนจะเกิด), composable (ประกอบกันได้ง่าย) และ cancellable (ยกเลิกได้)

pipe และ flow — เครื่องมือ Composition

import { pipe, Effect } from "effect"

// pipe — ส่งค่าผ่านฟังก์ชันทีละตัว (ซ้ายไปขวา)
const program = pipe(
  Effect.succeed(10),
  Effect.map(n => n * 2),        // 20
  Effect.map(n => n + 5),        // 25
  Effect.flatMap(n =>
    n > 20
      ? Effect.succeed(`Big: ${n}`)
      : Effect.fail(new Error("Too small"))
  )
)

// flow — สร้างฟังก์ชันใหม่จากการประกอบ
import { flow } from "effect"

const processNumber = flow(
  (n: number) => n * 2,
  (n) => n + 5,
  (n) => `Result: ${n}`
)
console.log(processNumber(10))  // "Result: 25"

// map vs flatMap
// map: A => B (transform ค่า ไม่เปลี่ยน Effect structure)
// flatMap: A => Effect (transform ค่า + อาจ fail/ต้องการ dependency)

Generators สำหรับ Effect — เขียนเหมือน async/await

Effect-TS รองรับ Generator syntax ที่ทำให้เขียน Effect code ได้เหมือน async/await ซึ่งอ่านง่ายกว่าการใช้ pipe กับ flatMap ซ้อนกัน

import { Effect } from "effect"

// แบบ pipe (functional style)
const programPipe = pipe(
  fetchUser("123"),
  Effect.flatMap(user =>
    pipe(
      fetchOrders(user.id),
      Effect.map(orders => ({ user, orders }))
    )
  ),
  Effect.flatMap(({ user, orders }) =>
    calculateTotal(orders)
  )
)

// แบบ Generator (imperative style — อ่านง่ายกว่า)
const programGen = Effect.gen(function* () {
  const user = yield* fetchUser("123")
  const orders = yield* fetchOrders(user.id)
  const total = yield* calculateTotal(orders)
  return { user, orders, total }
})

// ทั้งสองแบบให้ผลเหมือนกัน แต่ Generator อ่านง่ายกว่า
// yield* ทำหน้าที่เหมือน await ใน async/await
// แต่มี typed errors และ dependency tracking

Error Handling — Typed Errors และ Defects

Typed Errors (Expected Errors)

Effect-TS แยก Errors ออกเป็น 2 ประเภท คือ Expected Errors (Typed Errors) ที่ปรากฏใน Type signature และ Unexpected Errors (Defects) ที่เป็น bug จริงๆ

import { Effect, Data } from "effect"

// สร้าง Error types
class UserNotFound extends Data.TaggedError("UserNotFound")<{
  readonly userId: string
}> {}

class DatabaseError extends Data.TaggedError("DatabaseError")<{
  readonly message: string
}> {}

class ValidationError extends Data.TaggedError("ValidationError")<{
  readonly field: string
  readonly reason: string
}> {}

// Function ที่บอก Error type ชัดเจน
const getUser = (id: string): Effect.Effect<User, UserNotFound | DatabaseError> =>
  Effect.gen(function* () {
    const row = yield* queryDatabase(`SELECT * FROM users WHERE id = ${id}`)
    if (!row) return yield* new UserNotFound({ userId: id })
    return parseUser(row)
  })

// จัดการ Error เฉพาะประเภท
const program = pipe(
  getUser("123"),
  Effect.catchTag("UserNotFound", (e) =>
    Effect.succeed({ id: e.userId, name: "Guest", isGuest: true })
  ),
  // DatabaseError ยังคง propagate ขึ้นไป
  Effect.catchTag("DatabaseError", (e) =>
    Effect.fail(new ServiceUnavailable({ cause: e.message }))
  )
)

Defects (Unexpected Errors)

// Defect = bug ที่ไม่คาดคิด (เช่น null pointer, divide by zero)
// ไม่ปรากฏใน Type signature
// ใช้ Effect.die() หรือ throw ใน Effect code

const riskyOperation = Effect.sync(() => {
  // ถ้า throw ที่นี่ จะเป็น Defect (ไม่ใช่ typed error)
  if (Math.random() > 0.5) throw new Error("Random failure")
  return 42
})

// แปลง Defect เป็น typed error
const safe = pipe(
  riskyOperation,
  Effect.catchAllDefect((defect) =>
    Effect.fail(new UnexpectedError({ cause: String(defect) }))
  )
)

Dependency Injection ด้วย Layers

Effect-TS ใช้ระบบ Layer สำหรับ Dependency Injection ที่ type-safe ไม่ต้อง runtime DI container ไม่ต้อง decorators

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

// 1. กำหนด Service interface
class UserRepository extends Context.Tag("UserRepository")<
  UserRepository,
  {
    readonly findById: (id: string) => Effect.Effect<User, UserNotFound>
    readonly save: (user: User) => Effect.Effect<void, DatabaseError>
  }
>() {}

class EmailService extends Context.Tag("EmailService")<
  EmailService,
  {
    readonly send: (to: string, subject: string, body: string) => Effect.Effect<void, EmailError>
  }
>() {}

// 2. ใช้ Service ใน Effect
const registerUser = (name: string, email: string) =>
  Effect.gen(function* () {
    const repo = yield* UserRepository
    const mailer = yield* EmailService

    const user = { id: crypto.randomUUID(), name, email }
    yield* repo.save(user)
    yield* mailer.send(email, "Welcome!", `Hello ${name}!`)
    return user
  })
// Type: Effect
// TypeScript รู้ว่าต้องการ Dependencies อะไร!

// 3. สร้าง Layer (implementation)
const UserRepositoryLive = Layer.succeed(
  UserRepository,
  {
    findById: (id) => Effect.succeed({ id, name: "Test", email: "test@test.com" }),
    save: (user) => Effect.succeed(undefined)
  }
)

const EmailServiceLive = Layer.succeed(
  EmailService,
  {
    send: (to, subject, body) => Effect.succeed(undefined)
  }
)

// 4. Provide layers และ Run
const MainLive = Layer.merge(UserRepositoryLive, EmailServiceLive)

Effect.runPromise(
  pipe(
    registerUser("John", "john@example.com"),
    Effect.provide(MainLive)
  )
)
ข้อดีของ Layer: Dependencies ถูกตรวจสอบ ณ compile time ถ้าลืม provide Layer ใดๆ TypeScript จะ Error ทันที ไม่ต้องรอ runtime ถึงจะรู้ว่าลืม inject dependency

Concurrency — Fibers และ Structured Concurrency

import { Effect, Fiber } from "effect"

// รัน Effects พร้อมกัน
const parallel = Effect.all([
  fetchUser("1"),
  fetchUser("2"),
  fetchUser("3")
], { concurrency: "unbounded" })
// ผลลัพธ์: Effect<[User, User, User], Error>

// Structured concurrency — ถ้า parent ถูก cancel, children ก็ถูก cancel ด้วย
const withTimeout = pipe(
  longRunningTask,
  Effect.timeout("5 seconds")  // cancel ถ้าเกิน 5 วินาที
)

// Racing — เอาผลจากตัวที่เสร็จก่อน
const fastest = Effect.race(
  fetchFromPrimary,
  fetchFromBackup
)

// forEach with concurrency
const results = Effect.forEach(
  userIds,
  (id) => fetchUser(id),
  { concurrency: 5 }  // จำกัด 5 concurrent requests
)

// Fiber — lightweight thread
const program2 = Effect.gen(function* () {
  // Fork fiber (ทำงาน background)
  const fiber = yield* Effect.fork(longRunningTask)

  // ทำงานอื่นพร้อมกัน
  const quickResult = yield* quickTask

  // รอ fiber เสร็จ
  const longResult = yield* Fiber.join(fiber)

  return { quickResult, longResult }
})

Effect vs Promise — เปรียบเทียบ

คุณสมบัติPromiseEffect
ExecutionEager (ทำทันที)Lazy (ทำเมื่อ run)
Error Typeunknown (จับไม่ได้)Typed (E parameter)
Dependenciesไม่มี trackingR parameter (compile-time check)
Cancellationไม่รองรับ (AbortController ยุ่งยาก)Built-in (Fiber.interrupt)
Retryต้องเขียนเองEffect.retry (built-in)
ConcurrencyPromise.all/raceEffect.all + concurrency option
Composition.then() chainingpipe, generators, operators
Testingต้อง mock globalsProvide test Layer

Effect Schema — Validation + Serialization

import { Schema } from "@effect/schema"

// กำหนด Schema (ทั้ง validation + TypeScript type)
const UserSchema = Schema.Struct({
  id: Schema.String,
  name: Schema.String.pipe(Schema.minLength(1), Schema.maxLength(100)),
  email: Schema.String.pipe(Schema.pattern(/^[^@]+@[^@]+\.[^@]+$/)),
  age: Schema.Number.pipe(Schema.int(), Schema.between(0, 150)),
  role: Schema.Literal("admin", "user", "moderator")
})

// TypeScript type ถูกสร้างอัตโนมัติ
type User = typeof UserSchema.Type

// Decode (validate + transform)
const decodeUser = Schema.decodeUnknown(UserSchema)

const result = decodeUser({
  id: "123",
  name: "John",
  email: "john@example.com",
  age: 25,
  role: "user"
})
// Effect

Effect Platform — HTTP Client

import { HttpClient, HttpClientRequest } from "@effect/platform"

const getUser = (id: string) =>
  Effect.gen(function* () {
    const client = yield* HttpClient.HttpClient
    const response = yield* pipe(
      HttpClientRequest.get(`https://api.example.com/users/${id}`),
      client.execute,
      Effect.flatMap(r => r.json),
      Effect.flatMap(Schema.decodeUnknown(UserSchema))
    )
    return response
  })

// Retry with exponential backoff
const resilientGet = pipe(
  getUser("123"),
  Effect.retry({
    times: 3,
    schedule: Schedule.exponential("1 second")
  })
)

Testing Effect Programs

import { Effect, Layer } from "effect"

// Test Layer — ใช้ mock implementation
const UserRepositoryTest = Layer.succeed(
  UserRepository,
  {
    findById: (id) =>
      id === "existing"
        ? Effect.succeed({ id, name: "Test User", email: "test@test.com" })
        : Effect.fail(new UserNotFound({ userId: id })),
    save: (_user) => Effect.succeed(undefined)
  }
)

const EmailServiceTest = Layer.succeed(
  EmailService,
  {
    send: (to, subject, body) => {
      console.log(`[TEST] Email to ${to}: ${subject}`)
      return Effect.succeed(undefined)
    }
  }
)

const TestLayer = Layer.merge(UserRepositoryTest, EmailServiceTest)

// ทดสอบ
describe("registerUser", () => {
  it("should create user and send email", async () => {
    const result = await Effect.runPromise(
      pipe(
        registerUser("John", "john@example.com"),
        Effect.provide(TestLayer)
      )
    )
    expect(result.name).toBe("John")
  })
})

เมื่อไหร่ควรใช้ Effect-TS?

เหมาะสำหรับ

อาจไม่จำเป็นสำหรับ

สรุป

Effect-TS เป็น Library ที่เปลี่ยนวิธีคิดเรื่อง TypeScript development ด้วยแนวคิด Functional Programming ที่จริงจัง Type-safe error handling ที่ไม่ให้ Error หลุดรอด Dependency Injection ที่ตรวจสอบ ณ compile time และ Structured concurrency ที่จัดการ async ได้อย่างเป็นระบบ

แม้จะมี Learning curve ที่สูง แต่ผลลัพธ์คือ Code ที่แข็งแกร่ง ทดสอบง่าย maintain ง่าย และมี Bug น้อยลงอย่างมาก ถ้าคุณทำงานกับ TypeScript Backend ที่ซับซ้อน Effect-TS คือการลงทุนที่คุ้มค่าในระยะยาว


Back to Blog | iCafe Forex | SiamLanCard | Siam2R