Option type: eliminando null e undefined
1. O problema de null e undefined em TypeScript
Tony Hoare, inventor do conceito de referência nula, chamou-o de "erro de bilhões de dólares". Em TypeScript, null e undefined são fontes constantes de erros em runtime, especialmente quando integramos APIs externas, bancos de dados ou formulários complexos.
TypeScript oferece ferramentas para mitigar o problema: strictNullChecks no tsconfig.json, o operador de encadeamento opcional (?.) e o operador de coalescência nula (??). No entanto, essas soluções são reativas — tratam o problema quando ele aparece, mas não eliminam a raiz.
// Soluções atuais ainda permitem vazamentos
const user = await fetchUser(); // user: User | null
const city = user?.address?.city ?? 'Unknown'; // Ainda precisamos verificar
O problema se agrava em pipelines de dados: um null não tratado em uma etapa contamina todo o fluxo subsequente, criando bugs silenciosos que só aparecem em produção.
2. Fundamentos do Option type
O Option<T> é um tipo que representa a presença (Some<T>) ou ausência (None) de um valor. É uma implementação da monada Maybe da teoria das categorias, aplicada a linguagens funcionais.
Diferente de null, que é um valor "invisível" que pode surgir em qualquer lugar, Option torna explícita a possibilidade de ausência no sistema de tipos. O compilador força o desenvolvedor a tratar ambos os casos.
type Option<T> = Some<T> | None;
interface Some<T> {
readonly _tag: 'Some';
readonly value: T;
}
interface None {
readonly _tag: 'None';
}
A semântica é clara: se uma função retorna Option<User>, você sabe que pode não haver usuário. Não há ambiguidade.
3. Construindo um Option type do zero
Implementar um Option próprio nos dá controle total sobre o comportamento e evita dependências externas desnecessárias.
class Some<T> {
readonly _tag = 'Some' as const;
constructor(public readonly value: T) {}
}
class None {
readonly _tag = 'None' as const;
}
type Option<T> = Some<T> | None;
// Construtores
const some = <T>(value: T): Option<T> => new Some(value);
const none = <T>(): Option<T> => new None();
// Interoperabilidade com null/undefined
const fromNullable = <T>(value: T | null | undefined): Option<T> =>
value == null ? none() : some(value);
// Type guards
const isSome = <T>(option: Option<T>): option is Some<T> => option._tag === 'Some';
const isNone = <T>(option: Option<T>): option is None => option._tag === 'None';
A função fromNullable é essencial para integrar código legado que ainda usa null ou undefined.
4. Operações fundamentais para manipular Option
Com a estrutura básica pronta, podemos adicionar operações que transformam o valor sem sair do contexto seguro.
// Map: transforma o valor interno se presente
const map = <T, U>(option: Option<T>, fn: (value: T) => U): Option<U> =>
isSome(option) ? some(fn(option.value)) : none();
// FlatMap: evita Option aninhados
const flatMap = <T, U>(option: Option<T>, fn: (value: T) => Option<U>): Option<U> =>
isSome(option) ? fn(option.value) : none();
// Match/Fold: pattern matching para extrair valor
const match = <T, U>(
option: Option<T>,
onSome: (value: T) => U,
onNone: () => U
): U => (isSome(option) ? onSome(option.value) : onNone());
// UnwrapOr: valor com fallback
const getOrElse = <T>(option: Option<T>, defaultValue: T): T =>
isSome(option) ? option.value : defaultValue;
// Expect: extrai valor ou lança erro controlado
const expect = <T>(option: Option<T>, message: string): T => {
if (isNone(option)) throw new Error(message);
return option.value;
};
Essas operações formam a base para composição funcional segura.
5. Composição e encadeamento de operações
O verdadeiro poder do Option aparece no encadeamento de operações, eliminando verificações manuais e aninhamentos.
// Pipeline funcional
const processUser = (user: User): Option<string> =>
pipe(
fromNullable(user.email),
map(email => email.toLowerCase()),
flatMap(email => validateEmail(email) ? some(email) : none())
);
// Trabalhando com múltiplos Option
const Option = {
all: <T>(options: Option<T>[]): Option<T[]> => {
const result: T[] = [];
for (const opt of options) {
if (isNone(opt)) return none();
result.push(opt.value);
}
return some(result);
},
any: <T>(options: Option<T>[]): Option<T> => {
for (const opt of options) {
if (isSome(opt)) return opt;
}
return none();
}
};
// Integração com Promises
const fetchUserOption = async (id: string): Promise<Option<User>> => {
try {
const user = await api.getUser(id);
return fromNullable(user);
} catch {
return none();
}
};
O encadeamento elimina os famosos "pyramids of doom" de verificações aninhadas.
6. Interoperabilidade com código e bibliotecas existentes
Adaptar APIs externas é um dos casos de uso mais comuns para Option.
// Adaptando resposta de API
interface ApiResponse {
data: unknown;
error: string | null;
}
const safeParse = (response: ApiResponse): Option<User> =>
pipe(
fromNullable(response.data),
flatMap(data => {
if (typeof data !== 'object' || data === null) return none();
return some(data as User);
})
);
// Validação de formulários
const validateForm = (form: FormData): Option<ValidForm> => {
const name = fromNullable(form.get('name'));
const email = fromNullable(form.get('email'));
return pipe(
Option.all([name, email]),
map(([name, email]) => ({
name: name.toString(),
email: email.toString()
})),
flatMap(({ name, email }) =>
name.length > 0 && email.includes('@')
? some({ name, email })
: none()
)
);
};
7. Padrões avançados e aplicações reais
Em coleções, Option permite operações elegantes como compact (filtrar None).
// Compact: remove None de arrays
const compact = <T>(options: Option<T>[]): T[] =>
options.reduce((acc: T[], opt) => {
if (isSome(opt)) acc.push(opt.value);
return acc;
}, []);
// Combinação com Result type
type Result<T, E> = Success<T, E> | Failure<T, E>;
// Option<Result<T, E>>: operação que pode falhar ou não existir
// Result<Option<T>, E>: operação que sempre executa, mas pode não ter valor
const processOptionalResult = (input: string): Option<Result<number, string>> => {
if (input === '') return none();
const num = parseInt(input);
return some(isNaN(num) ? failure('Invalid number') : success(num));
};
8. Performance, boas práticas e considerações finais
O Option type adiciona overhead de alocação de objetos. Em loops críticos de performance, considere usar null com verificações explícitas. Para a maioria dos casos, porém, o ganho em segurança compensa.
Quando NÃO usar Option:
- Limites do sistema (interfaces com bibliotecas que esperam null)
- Performance crítica com milhões de operações
- Dados que são garantidamente sempre presentes (use tipo direto)
Ecossistema e bibliotecas recomendadas:
- fp-ts: implementação completa com suporte a Either, Task, etc.
- neverthrow: foco em Result type, mas inclui Option
- oxide.ts: inspirado no Rust, implementação leve
- Proposta TC39 para Optional (estágio 1): pode trazer suporte nativo ao JavaScript
O Option type não é uma bala de prata, mas uma ferramenta poderosa para eliminar a ambiguidade de null e undefined em TypeScript, promovendo código mais previsível e seguro.
Referências
- TypeScript Handbook: Nullable Types — Documentação oficial sobre tratamento de null e undefined no TypeScript
- fp-ts Option — Implementação completa do Option type na biblioteca fp-ts, com exemplos de uso
- neverthrow: Option type — Biblioteca focada em Result e Option types, com integração assíncrona
- oxide.ts: Option and Result — Implementação inspirada no Rust, leve e focada em TypeScript moderno
- Proposta TC39: Optional Chaining — Proposta oficial para adicionar suporte nativo ao Option pattern em JavaScript/TypeScript
- Monada Maybe em TypeScript: Teoria e Prática — Artigo técnico explicando os fundamentos teóricos da monada Maybe aplicada ao Option type