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 ที่ทำงานทันทีที่สร้าง
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)
)
)
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 — เปรียบเทียบ
| คุณสมบัติ | Promise | Effect |
|---|---|---|
| Execution | Eager (ทำทันที) | Lazy (ทำเมื่อ run) |
| Error Type | unknown (จับไม่ได้) | Typed (E parameter) |
| Dependencies | ไม่มี tracking | R parameter (compile-time check) |
| Cancellation | ไม่รองรับ (AbortController ยุ่งยาก) | Built-in (Fiber.interrupt) |
| Retry | ต้องเขียนเอง | Effect.retry (built-in) |
| Concurrency | Promise.all/race | Effect.all + concurrency option |
| Composition | .then() chaining | pipe, generators, operators |
| Testing | ต้อง mock globals | Provide 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?
เหมาะสำหรับ
- Backend services ที่ซับซ้อน (หลาย dependencies, หลาย error types)
- แอปที่ต้องการ reliability สูง (retry, circuit breaker, timeout)
- ทีมที่ต้องการ type-safe error handling
- โปรเจกต์ที่มี complex business logic
- Microservices ที่ต้อง compose หลาย services เข้าด้วยกัน
อาจไม่จำเป็นสำหรับ
- Frontend/UI ง่ายๆ (React components, form handling)
- Script เล็กๆ ที่ทำครั้งเดียว
- ทีมที่ไม่คุ้นเคยกับ FP concepts
- โปรเจกต์ที่ต้องการ learning curve ต่ำ
สรุป
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 คือการลงทุนที่คุ้มค่าในระยะยาว
