Como usar o Effect-TS para modelar erros e dependências em TypeScript
1. Introdução ao Effect-TS e seus conceitos fundamentais
Effect-TS é uma biblioteca funcional para TypeScript que oferece uma abordagem robusta para gerenciar efeitos colaterais, erros e dependências de forma tipada e composicional. Diferente de Promise, que trata qualquer rejeição como unknown, ou Try, que captura exceções genéricas, o tipo Effect<Success, Error, Requirements> permite declarar explicitamente o tipo de sucesso, o tipo de erro e as dependências necessárias para executar um efeito.
Para configurar um projeto com Effect-TS, instale o pacote principal:
npm install effect
Em seguida, importe os tipos e funções básicas:
import { Effect, pipe } from "effect";
O tipo Effect é a unidade central. Um efeito que lê um arquivo e pode falhar com um erro FileNotFound depende de um serviço FileSystem:
type FileNotFound = { readonly _tag: "FileNotFound"; readonly path: string };
type ReadFile = Effect<string, FileNotFound, FileSystem>;
2. Modelagem de erros com efeitos tipados
Erros previsíveis são declarados diretamente no tipo do efeito. Use Either para representar sucesso ou falha sem exceções, Option para valores opcionais e Cause para erros complexos com múltiplas causas.
Exemplo de validação de entrada:
type ValidationError =
| { readonly _tag: "EmptyString" }
| { readonly _tag: "InvalidEmail" };
function validateEmail(input: string): Effect<string, ValidationError, never> {
if (input.length === 0) return Effect.fail({ _tag: "EmptyString" });
if (!input.includes("@")) return Effect.fail({ _tag: "InvalidEmail" });
return Effect.succeed(input);
}
Tratamento de erros com catchAll, catchTag e orElse:
const program = pipe(
validateEmail(""),
Effect.catchTag("EmptyString", () => Effect.succeed("fallback@example.com")),
Effect.catchAll((error) => Effect.succeed(`Erro tratado: ${error._tag}`))
);
catchTag permite tratar erros específicos por sua tag, enquanto catchAll captura qualquer erro restante.
3. Gerenciamento de dependências e injeção de serviços
O terceiro parâmetro genérico Requirements define as dependências do efeito. Crie serviços com Context.Tag e Layer:
import { Context, Layer } from "effect";
class DatabaseService extends Context.Tag("DatabaseService")<
DatabaseService,
{ readonly query: (sql: string) => Effect<any, Error, never> }
>() {}
class LoggerService extends Context.Tag("LoggerService")<
LoggerService,
{ readonly log: (message: string) => Effect<void, never, never> }
>() {}
Defina implementações concretas:
const DatabaseLive = Layer.succeed(DatabaseService, {
query: (sql) => Effect.succeed(`Resultado de: ${sql}`)
});
const LoggerLive = Layer.succeed(LoggerService, {
log: (message) => Effect.sync(() => console.log(message))
});
Componha camadas com Layer.mergeAll e forneça ao efeito principal:
const MainLayer = Layer.mergeAll(DatabaseLive, LoggerLive);
const app = Effect.flatMap(
DatabaseService,
(db) => db.query("SELECT * FROM users")
);
const runnable = Layer.provide(app, MainLayer);
4. Composição e encadeamento de efeitos
Operadores como map, flatMap, zip e forEach permitem compor efeitos de forma declarativa. Use pipe para encadear operações:
const fetchUser = (id: number): Effect<{ name: string }, Error, HttpClient> =>
Effect.succeed({ name: "Alice" });
const saveLog = (msg: string): Effect<void, Error, LoggerService> =>
Effect.flatMap(LoggerService, (logger) => logger.log(msg));
const workflow = pipe(
fetchUser(1),
Effect.flatMap((user) => saveLog(`Usuário ${user.name} carregado`)),
Effect.map(() => "Processo concluído")
);
Exemplo prático: fluxo de validação + persistência com tratamento de erros:
const createUser = (data: { email: string }) =>
pipe(
validateEmail(data.email),
Effect.flatMap((email) =>
Effect.flatMap(DatabaseService, (db) => db.query(`INSERT INTO users (email) VALUES ('${email}')`))
),
Effect.catchTag("EmptyString", () => Effect.fail({ _tag: "InvalidInput" })),
Effect.catchAll((error) =>
Effect.flatMap(LoggerService, (logger) => logger.log(`Falha: ${error._tag}`))
)
);
5. Testabilidade e simulação de dependências
Substitua serviços reais por TestLayer para testes isolados:
import { TestContext } from "effect/testing";
const DatabaseTest = Layer.succeed(DatabaseService, {
query: (sql) => Effect.succeed(`Mock: ${sql}`)
});
const testLayer = Layer.mergeAll(DatabaseTest, LoggerLive);
Use Effect.runPromise para testes assíncronos e Effect.runSync para síncronos:
const testProgram = pipe(
createUser({ email: "test@example.com" }),
Layer.provide(testLayer),
Effect.runPromise
);
// Resultado: "Mock: INSERT INTO users (email) VALUES ('test@example.com')"
Isole erros com Effect.either para inspecionar falhas sem propagação:
const result = Effect.runSync(
pipe(
validateEmail("invalido"),
Effect.either
)
);
// result é Either<ValidationError, string>
Effect.sandbox expõe a Cause completa para depuração avançada.
6. Padrões avançados: retry, timeout e concorrência
Políticas de retry com Schedule:
import { Schedule } from "effect";
const retryPolicy = pipe(
Schedule.exponential("100 millis"),
Schedule.compose(Schedule.recurs(3))
);
const withRetry = pipe(
Effect.fail("temporary error"),
Effect.retry(retryPolicy)
);
Timeout e cancelamento:
const timeoutEffect = pipe(
Effect.sleep("5 seconds"),
Effect.timeout("1 second"),
Effect.catchAll(() => Effect.succeed("Timeout ocorreu"))
);
Execução concorrente segura:
const task1 = Effect.succeed("A");
const task2 = Effect.succeed("B");
const parallel = Effect.zipPar(task1, task2);
// Executa ambas simultaneamente e combina resultados
const items = [1, 2, 3];
const parallelForEach = Effect.forEachPar(items, (n) => Effect.succeed(n * 2));
// Processa todos em paralelo
7. Caso de uso completo: API REST com dependências e erros modelados
Definição de serviços:
class HttpClient extends Context.Tag("HttpClient")<
HttpClient,
{ readonly get: (url: string) => Effect<string, Error, never> }
>() {}
class Database extends Context.Tag("Database")<
Database,
{ readonly findUser: (id: number) => Effect<{ name: string } | null, Error, never> }
>() {}
class Logger extends Context.Tag("Logger")<
Logger,
{ readonly info: (msg: string) => Effect<void, never, never> }
>() {}
Endpoint que lida com erros de domínio e infraestrutura:
type ApiError =
| { readonly _tag: "UserNotFound" }
| { readonly _tag: "ExternalServiceError" }
| { readonly _tag: "DatabaseError" };
const getUserProfile = (userId: number): Effect<string, ApiError, HttpClient | Database | Logger> =>
pipe(
Effect.flatMap(Database, (db) => db.findUser(userId)),
Effect.catchAll(() => Effect.fail({ _tag: "DatabaseError" })),
Effect.flatMap((user) =>
user
? Effect.succeed(user)
: Effect.fail({ _tag: "UserNotFound" })
),
Effect.flatMap((user) =>
pipe(
Effect.flatMap(HttpClient, (http) => http.get(`https://api.example.com/users/${user.name}`)),
Effect.catchAll(() => Effect.fail({ _tag: "ExternalServiceError" }))
)
),
Effect.tap((response) =>
Effect.flatMap(Logger, (logger) => logger.info(`Perfil carregado: ${response}`))
)
);
Montagem final:
const HttpLive = Layer.succeed(HttpClient, { get: (url) => Effect.succeed(`dados de ${url}`) });
const DbLive = Layer.succeed(Database, { findUser: (id) => Effect.succeed({ name: "Alice" }) });
const LoggerLive = Layer.succeed(Logger, { info: (msg) => Effect.sync(() => console.log(msg)) });
const AppLayer = Layer.mergeAll(HttpLive, DbLive, LoggerLive);
const app = pipe(
getUserProfile(1),
Layer.provide(AppLayer),
Effect.runPromise
);
// Log: "Perfil carregado: dados de https://api.example.com/users/Alice"
Effect-TS transforma a modelagem de erros e dependências em uma experiência tipada, previsível e testável, eliminando surpresas em tempo de execução e promovendo código mais seguro e expressivo.
Referências
- Documentação oficial do Effect-TS — Guia completo sobre instalação, tipos e operadores fundamentais.
- Effect-TS GitHub Repository — Código-fonte, exemplos e issues da biblioteca.
- Effect-TS: Gerenciamento de Erros com Either e Cause — Artigo técnico sobre modelagem de erros com Either e Cause.
- Effect-TS: Injeção de Dependências com Layers — Tutorial prático sobre criação e composição de serviços com Layers.
- Effect-TS: Testes com TestContext e TestLayer — Documentação oficial sobre testes unitários e simulação de dependências.
- Effect-TS: Padrões de Retry e Schedule — Guia sobre políticas de retry, schedules e timeout.
- Effect-TS: Concorrência com zipPar e forEachPar — Exemplos de execução concorrente segura e paralelismo.