Erros como valores: Result type em TypeScript
1. Por que tratar erros como valores?
O tratamento de erros em TypeScript tradicionalmente depende de try/catch e exceções. Essa abordagem apresenta limitações significativas:
- Exceções não são tipadas: qualquer função pode lançar qualquer coisa, sem garantias de tipo
- Propagação implícita: erros podem escapar sem serem declarados na assinatura da função
null/undefinedcomo indicadores: forçam verificações manuais e podem causar erros em tempo de execução
// Problema: função não indica que pode falhar
function dividir(a: number, b: number): number {
return a / b; // Pode retornar Infinity, não indica erro de divisão por zero
}
// Problema: null como indicador de erro
function buscarUsuario(id: number): Usuario | null {
// ... implementação
}
Vantagens de um tipo explícito para sucesso e falha:
- Erros são declarados na assinatura da função
- O compilador força o tratamento de todos os casos
- Código mais previsível e auto-documentado
2. Implementando um Result type do zero
Vamos construir nosso próprio Result usando uma união discriminada:
type Result<T, E = Error> = Ok<T, E> | Err<T, E>;
class Ok<T, E> {
readonly _tag = 'Ok';
constructor(readonly value: T) {}
isOk(): this is Ok<T, E> {
return true;
}
isErr(): this is Err<T, E> {
return false;
}
unwrap(): T {
return this.value;
}
unwrapOr(defaultValue: T): T {
return this.value;
}
}
class Err<T, E> {
readonly _tag = 'Err';
constructor(readonly error: E) {}
isOk(): this is Ok<T, E> {
return false;
}
isErr(): this is Err<T, E> {
return true;
}
unwrap(): never {
throw new Error(`Tentou desencapsular um Err: ${this.error}`);
}
unwrapOr(defaultValue: T): T {
return defaultValue;
}
}
// Funções utilitárias
function ok<T, E = never>(value: T): Result<T, E> {
return new Ok(value);
}
function err<T = never, E = Error>(error: E): Result<T, E> {
return new Err(error);
}
function fromThrowable<T, E = Error>(
fn: () => T,
errorMapper?: (error: unknown) => E
): Result<T, E> {
try {
return ok(fn());
} catch (error) {
return err(errorMapper ? errorMapper(error) : error as E);
}
}
3. Trabalhando com Result em cadeias de operações
Para tornar o Result verdadeiramente útil, adicionamos métodos de transformação:
// Métodos na classe Ok
class Ok<T, E> {
// ... métodos anteriores
map<U>(fn: (value: T) => U): Result<U, E> {
return ok(fn(this.value));
}
mapErr<F>(_fn: (error: E) => F): Result<T, F> {
return ok(this.value);
}
andThen<U>(fn: (value: T) => Result<U, E>): Result<U, E> {
return fn(this.value);
}
orElse<F>(_fn: (error: E) => Result<T, F>): Result<T, E> {
return ok(this.value);
}
}
// Métodos na classe Err
class Err<T, E> {
// ... métodos anteriores
map<U>(_fn: (value: T) => U): Result<U, E> {
return err(this.error);
}
mapErr<F>(fn: (error: E) => F): Result<T, F> {
return err(fn(this.error));
}
andThen<U>(_fn: (value: T) => Result<U, E>): Result<U, E> {
return err(this.error);
}
orElse<F>(fn: (error: E) => Result<T, F>): Result<T, F> {
return fn(this.error as unknown as F);
}
}
Exemplo prático de encadeamento:
function parseJSON(json: string): Result<unknown, string> {
return fromThrowable(
() => JSON.parse(json),
() => 'JSON inválido'
);
}
function validateUser(data: unknown): Result<{name: string; age: number}, string> {
if (typeof data !== 'object' || data === null) {
return err('Dados não são um objeto');
}
// ... validações adicionais
return ok({ name: 'João', age: 30 });
}
function processUser(json: string): Result<string, string> {
return parseJSON(json)
.andThen(validateUser)
.map(user => `Usuário: ${user.name}, ${user.age} anos`);
}
// Uso
const result = processUser('{"name":"João","age":30}');
if (result.isOk()) {
console.log(result.unwrap()); // "Usuário: João, 30 anos"
} else {
console.error(`Erro: ${result.unwrapOr('')}`);
}
4. Padrões de uso em aplicações reais
Validação de formulários
interface FormData {
email: string;
password: string;
}
function validateEmail(email: string): Result<string, string> {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email)
? ok(email)
: err('Email inválido');
}
function validatePassword(password: string): Result<string, string> {
return password.length >= 8
? ok(password)
: err('Senha deve ter no mínimo 8 caracteres');
}
function validateForm(data: FormData): Result<FormData, string[]> {
const errors: string[] = [];
const emailResult = validateEmail(data.email);
if (emailResult.isErr()) errors.push(emailResult.unwrapOr(''));
const passwordResult = validatePassword(data.password);
if (passwordResult.isErr()) errors.push(passwordResult.unwrapOr(''));
return errors.length === 0 ? ok(data) : err(errors);
}
Chamadas a APIs
async function fetchUser(id: number): Promise<Result<User, NetworkError>> {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
return err(new NetworkError(`HTTP ${response.status}`));
}
const data = await response.json();
return ok(data as User);
} catch (error) {
return err(new NetworkError('Falha na conexão'));
}
}
// Uso com async/await
async function getUserDisplay(id: number): Promise<Result<string, string>> {
const result = await fetchUser(id);
return result
.map(user => `Nome: ${user.name}, Email: ${user.email}`)
.mapErr(err => `Erro ao buscar usuário: ${err.message}`);
}
5. Integração com bibliotecas e ecossistema
Bibliotecas populares como neverthrow e oxide.ts oferecem implementações robustas:
// Exemplo com neverthrow
import { Result, ok, err } from 'neverthrow';
function parseConfig(config: string): Result<Config, string> {
return fromThrowable(
() => JSON.parse(config) as Config,
() => 'Configuração inválida'
);
}
// Adaptação de funções que lançam exceções
function safeJSONParse<T>(json: string): Result<T, string> {
return fromThrowable(
() => JSON.parse(json) as T,
() => 'JSON inválido'
);
}
// Interoperabilidade com Promises
async function safeFetch<T>(url: string): Promise<Result<T, string>> {
try {
const response = await fetch(url);
if (!response.ok) return err(`HTTP ${response.status}`);
const data = await response.json();
return ok(data as T);
} catch {
return err('Erro de rede');
}
}
6. Comparação com alternativas comuns
Result vs Either
Either é mais genérico (Left/Right), enquanto Result é semanticamente focado em sucesso/erro:
// Either (genérico)
type Either<L, R> = Left<L> | Right<R>;
// Result (semântico)
type Result<T, E> = Ok<T> | Err<E>;
Result vs Option/Maybe
Option representa ausência de valor, Result representa falha com informação:
type Option<T> = Some<T> | None; // Apenas presença/ausência
type Result<T, E> = Ok<T> | Err<E>; // Sucesso com valor ou erro com detalhes
Quando usar cada abordagem
- Use
Result: operações que podem falhar com múltiplos tipos de erro - Use exceções: erros críticos que não devem ser ignorados (ex: falta de memória)
- Use
Option: quando a ausência de valor é esperada (ex: busca em cache)
7. Boas práticas e armadilhas
Evitando unwrap() cegamente
// Ruim: pode lançar exceção em tempo de execução
const valor = resultado.unwrap();
// Bom: tratamento explícito
if (resultado.isOk()) {
const valor = resultado.unwrap();
} else {
const erro = resultado.unwrapOr(new Error('Valor padrão'));
}
Tipagem correta de Err
// Especificar tipos de erro melhora a documentação
type ApiError = { code: number; message: string };
type ValidationError = { field: string; message: string };
function saveUser(user: User): Result<boolean, ApiError | ValidationError> {
// ...
}
Performance e legibilidade
Resultadiciona overhead de alocação de objetos- Para operações críticas, considere usar exceções
- Mantenha o encadeamento simples para evitar código difícil de ler
// Equilíbrio entre segurança e legibilidade
function processData(input: string): Result<number, string> {
return safeJSONParse(input)
.andThen(validateStructure)
.map(transformData);
}
Referências
- neverthrow - Documentação oficial — Biblioteca TypeScript para Result types com exemplos práticos e API completa
- oxide.ts - Result and Option types — Implementação funcional de Result e Option para TypeScript
- TypeScript Handbook - Discriminated Unions — Documentação oficial sobre uniões discriminadas, base para implementação de Result
- fp-ts - Either type documentation — Implementação de Either em fp-ts, biblioteca de programação funcional para TypeScript
- Error Handling in TypeScript with Result Types — Artigo técnico detalhado sobre tratamento de erros com Result types em TypeScript
- Rust Result Type - The Rust Reference — Documentação do Result type em Rust, inspiração para implementações em TypeScript
- TypeScript Deep Dive - Error Handling — Guia avançado sobre TypeScript incluindo tratamento de erros com uniões discriminadas