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 0Diferente 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.