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