Aprenda / TypeScript do zero

IntermediárioAula 9 de 1415 min0% da trilha

Enums, literal types e discriminated unions

enum parece o jeito 'oficial' de listar opções em TypeScript — na prática, boa parte do código moderno prefere string literal types.

Quando você precisa representar "um valor entre um conjunto fixo de opções" — status de um pedido, tipo de pagamento, papel de um usuário — o TypeScript te dá duas ferramentas parecidas na superfície, mas com comportamentos bem diferentes por baixo: enum e union de string literals.

enum — o jeito "clássico"

enum Status {
  Pendente,
  Pago,
  Cancelado,
}

const statusAtual: Status = Status.Pago;

console.log(Status.Pago); // 1 — por padrão, enum numérico começa em 0

Diferente de interface e type, um enum gera código JavaScript de verdade (um objeto) que existe em runtime — não é só uma checagem que desaparece na compilação. E um enum numérico tem uma pegadinha: por padrão, o TypeScript aceita qualquer number onde um Status é esperado, mesmo que não corresponda a nenhum membro declarado.

String literal union — a alternativa mais usada hoje

type Status = "pendente" | "pago" | "cancelado";

const statusAtual: Status = "pago";

function atualizarStatus(status: Status) {
  // autocomplete mostra exatamente as 3 opções — e só essas
}

atualizarStatus("enviado");
// Error: Argument of type '"enviado"' is not assignable to parameter of type 'Status'.

Uma union de string literals não gera nenhum código extra em runtime (some na compilação, como qualquer outro tipo) e o valor que aparece no editor/debugger já é legível ("pago", não 1). Por isso é a opção mais comum em código TypeScript moderno, especialmente em projetos que trafegam esses valores como JSON entre frontend e API.

Discriminated union com mais de duas variantes

Você já viu o formato básico de discriminated union (sucesso/erro). O mesmo padrão escala pra quantas variantes forem necessárias, contanto que exista um campo em comum — o discriminante — que muda de valor entre elas:

type EstadoRequisicao<T> =
  | { kind: "carregando" }
  | { kind: "sucesso"; dado: T }
  | { kind: "erro"; mensagem: string };

function renderizar(estado: EstadoRequisicao<string[]>) {
  switch (estado.kind) {
    case "carregando":
      return "Carregando...";
    case "sucesso":
      return estado.dado.join(", "); // TS sabe que "dado" existe só aqui
    case "erro":
      return `Erro: ${estado.mensagem}`; // e "mensagem" só aqui
  }
}

Exhaustiveness checking — garantir que nenhum caso ficou de fora

Um truque comum: no default de um switch sobre um discriminante, atribuir o valor a uma variável do tipo never. Se alguém adicionar uma variante nova ao union e esquecer de tratá-la no switch, o TypeScript acusa erro de compilação — antes de virar bug em produção:

function renderizarCompleto(estado: EstadoRequisicao<string[]>): string {
  switch (estado.kind) {
    case "carregando":
      return "Carregando...";
    case "sucesso":
      return estado.dado.join(", ");
    case "erro":
      return `Erro: ${estado.mensagem}`;
    default:
      const _exaustivo: never = estado; // se sobrar algum "kind" não tratado, erro aqui
      return _exaustivo;
  }
}

Exercício

Modele um discriminated union Pagamento com 3 variantes pelo campo metodo: "pix" (com chave: string), "cartao" (com parcelas: number) e "boleto" (com vencimento: string). Escreva uma função que recebe um Pagamento e retorna uma descrição em texto, usando um switch sobre metodo.

Quiz rápido

Por que um enum numérico do TypeScript pode gerar confusão que uma union de string literals não gera?
Num switch sobre o campo discriminante de uma discriminated union, o que o TypeScript faz dentro de cada case?