Effect-TS: programação funcional tipada que está dividindo a comunidade TypeScript

1. O que é Effect-TS e por que está gerando debate?

O TypeScript evoluiu de forma notável desde sua criação. No início, bibliotecas como lodash/fp tentavam trazer um pouco de imutabilidade e composição funcional para o ecossistema JavaScript. Depois veio o fp-ts, que introduziu conceitos como Option, Either e Task, oferecendo uma abordagem mais rigorosa de programação funcional tipada. No entanto, fp-ts sempre foi visto como uma biblioteca de conceitos — excelente para aprender, mas difícil de usar em produção sem um arcabouço mais completo.

Effect-TS surge exatamente nessa lacuna. Inspirado no ZIO (biblioteca de efeitos para Scala), Effect-TS propõe algo ambicioso: um sistema de efeitos algébricos completo para TypeScript, onde cada operação com efeito colateral é representada como um valor puro, composto e gerenciado por um runtime controlado.

A polêmica? Effect-TS promete pureza funcional e previsibilidade total, mas entrega uma complexidade cognitiva que muitos desenvolvedores consideram exagerada. A curva de aprendizado é íngreme, e o ecossistema, embora crescente, ainda não é mainstream. Para alguns, é o futuro do TypeScript reativo; para outros, um overengineering desnecessário.

2. Conceitos fundamentais do Effect-TS

O coração do Effect-TS é o tipo Effect<Requirements, Error, Value>. Diferente de uma Promise, que representa um valor futuro que pode falhar, um Effect é uma descrição lazy de uma computação que:

  • Requirements: declara as dependências necessárias para executar o efeito (ex: um serviço de banco de dados)
  • Error: tipa explicitamente os erros que podem ocorrer
  • Value: tipa o valor de sucesso

Essa tríade substitui promessas e try/catch de forma mais segura:

// Exemplo de efeito tipado
import { Effect } from "effect"

const getUser: Effect.Effect<Database, AppError, User> = 
  Effect.gen(function* (_) {
    const db = yield* _(Database)
    const user = yield* _(db.query("SELECT * FROM users WHERE id = ?", [1]))
    if (!user) {
      return yield* _(Effect.fail(new AppError("User not found")))
    }
    return user
  })

Efeitos são valores de primeira classe: podem ser compostos, transformados e passados como argumentos. A lazy evaluation garante que nada é executado até que o runtime decida. Isso traz referential transparency: a mesma expressão sempre produz o mesmo resultado, facilitando testes e raciocínio.

O sistema de Requirements é particularmente inovador. Em vez de depender de injeção de dependência manual ou containers IoC, o Effect-TS usa o próprio sistema de tipos para declarar o que um efeito precisa:

// Serviço injetável
class Database extends Effect.Service<Database>()("Database", {
  effect: Effect.gen(function* (_) {
    const pool = yield* _(createPool)
    return {
      query: (sql: string, params: any[]) => pool.query(sql, params)
    }
  })
}) {}

3. Como Effect-TS difere de abordagens tradicionais

Promises vs. Effect

Promises são eager: assim que criadas, começam a executar. Effect é lazy: você constrói um programa e só executa quando chama Effect.runPromise ou Effect.runSync. Isso permite composição segura e cancelamento.

RxJS vs. Effect

RxJS lida com streams de eventos ao longo do tempo. Effect foca em computações únicas (síncronas ou assíncronas) com concorrência estruturada. Effect pode fazer streams via Stream, mas não é seu propósito principal.

fp-ts vs. Effect

fp-ts oferece monadas tradicionais (TaskEither, Option), mas sem runtime integrado. Effect unifica tudo: efeitos, erros, dependências e concorrência em um único sistema com runtime configurável.

4. A divisão na comunidade TypeScript

A favor

  • Previsibilidade: sem efeitos colaterais implícitos
  • Testabilidade: dependências são injetadas e trocadas facilmente
  • Tipagem forte de erros: o compilador garante que você trata todos os casos de falha
  • Concorrência estruturada: sem memory leaks de promises não tratadas

Contra

  • Verbosidade: código funcional tipado exige mais anotações
  • Complexidade de tipos: tipos como Effect<A, B, C> podem assustar iniciantes
  • Custo de abstração: para times pequenos ou projetos simples, o overhead não compensa

Casos reais

Effect-TS brilha em sistemas críticos (microsserviços financeiros, pipelines de dados, sistemas de mensageria). É overengineering para CRUD simples, scripts de automação ou MVPs.

5. Ecossistema e ferramentas do Effect-TS

O ecossistema inclui:

  • Effect: núcleo com runtime, concorrência, fibra e scheduling
  • Schema: validação e serialização tipada de dados
  • Stream: processamento de streams com backpressure
  • Queue: filas concorrentes para comunicação entre fibras
  • Clock: abstração para tempo (facilita testes com time mocking)
  • Runtime configurável: permite logging, tracing e métricas embutidas

Integração com frameworks: existem adaptadores para Express, Fastify e suporte nativo a Vitest/Jest via Effect.runPromise.

6. Exemplo prático: migrando um handler HTTP de Express para Effect-TS

Código legado com Promises

// handler.ts (legado)
import { Request, Response } from "express"
import { getUserById } from "./db"
import { sendEmail } from "./email"

export const handler = async (req: Request, res: Response) => {
  try {
    const user = await getUserById(req.params.id)
    if (!user) {
      return res.status(404).json({ error: "User not found" })
    }
    await sendEmail(user.email, "Welcome back!")
    res.json({ message: "Email sent" })
  } catch (error) {
    res.status(500).json({ error: "Internal server error" })
  }
}

Refatoração para Effect-TS

// handler.effect.ts
import { Effect } from "effect"
import { Request, Response } from "express"

class UserNotFound {
  readonly _tag = "UserNotFound"
  constructor(readonly id: string) {}
}

class EmailSendFailed {
  readonly _tag = "EmailSendFailed"
  constructor(readonly error: unknown) {}
}

const getUserEffect = (id: string) =>
  Effect.gen(function* (_) {
    const user = yield* _(getUserById(id))
    if (!user) return yield* _(Effect.fail(new UserNotFound(id)))
    return user
  })

const sendEmailEffect = (email: string) =>
  Effect.gen(function* (_) {
    return yield* _(Effect.tryPromise({
      try: () => sendEmail(email, "Welcome back!"),
      catch: (error) => new EmailSendFailed(error)
    }))
  })

export const handler = async (req: Request, res: Response) => {
  const program = Effect.gen(function* (_) {
    const user = yield* _(getUserEffect(req.params.id))
    yield* _(sendEmailEffect(user.email))
    return { message: "Email sent" }
  })

  const result = await Effect.runPromise(
    Effect.catchAll(program, (error) => {
      if (error instanceof UserNotFound) {
        return Effect.succeed({ error: "User not found" })
      }
      return Effect.succeed({ error: "Internal server error" })
    })
  )

  res.json(result)
}

Teste unitário sem mockar dependências

// handler.test.ts
import { Effect, Layer } from "effect"
import { describe, it, expect } from "vitest"

const testDb = Layer.succeed(Database, {
  query: () => Promise.resolve({ id: 1, email: "test@test.com" })
})

const testEmail = Layer.succeed(EmailService, {
  send: () => Promise.resolve(true)
})

const testLayer = Layer.merge(testDb, testEmail)

it("should send email for existing user", async () => {
  const program = getUserEffect("1").pipe(
    Effect.flatMap((user) => sendEmailEffect(user.email)),
    Effect.provide(testLayer)
  )
  const result = await Effect.runPromise(program)
  expect(result).toBe(true)
})

7. Desafios e boas práticas para adoção gradual

Armadilhas comuns

  • Efeitos não executados: esquecer de chamar Effect.runPromise ou Effect.runFork
  • Tipos gigantes: composição excessiva sem quebrar em funções menores
  • Efeito vazamento: dependências não declaradas escapando via closures

Estratégias de adoção

Comece por uma camada de infraestrutura (banco de dados, HTTP client) e depois expanda para regras de negócio. Não tente migrar tudo de uma vez.

Cultura de equipe

Invista em documentação, pair programming e tenha um "campeão" do Effect-TS no time. A complexidade inicial é real, mas o retorno em previsibilidade e testabilidade compensa em projetos de médio/longo prazo.

8. Futuro do Effect-TS e o impacto no ecossistema TypeScript

O roadmap oficial inclui Effect v4 com melhorias significativas na DX (Developer Experience): redução de boilerplate, macros para Effect.gen e integração mais fluida com TypeScript 5.x.

Effect-TS já influencia frameworks maiores. A tendência de runtime funcional está presente em iniciativas como o Next.js (com Server Actions puros) e o Remix (com loaders sem efeitos colaterais implícitos).

A previsão? Effect-TS tem potencial para se tornar padrão de facto para sistemas reativos em TypeScript, especialmente em projetos que exigem alta confiabilidade. Mas continuará sendo nicho para equipes que dominam programação funcional. O meio-termo pode ser bibliotecas como ts-pattern e neverthrow, que oferecem parte dos benefícios sem a complexidade total.


Referências