in e discriminated unions: o coração do TS do dia a dia
Um objeto que representa 'carregando', 'sucesso' ou 'erro' é um dos padrões mais comuns de qualquer aplicação real. Discriminated unions modelam esse padrão com segurança total.
Estudar esta lição interativa →Às vezes dois formatos de objeto não têm nenhum tipo primitivo para diferenciar com typeof, os dois são objetos. Nesse caso, o operador in pergunta, em tempo real, se uma propriedade específica existe naquele objeto, e o TypeScript usa a resposta para o narrowing:
// narrowing com "in": dois formatos de objeto, diferenciados por uma
// propriedade que só um dos dois tem.
interface Gato { patas: number; ronrona: true }
interface Passaro { patas: number; voa: true }
function descreverAnimal(animal: Gato | Passaro) {
if ("voa" in animal) {
return "Um pássaro com " + animal.patas + " patas, que voa.";
}
return "Um gato com " + animal.patas + " patas, que ronrona.";
}
console.log(descreverAnimal({ patas: 2, voa: true }));
console.log(descreverAnimal({ patas: 4, ronrona: true }));Isso funciona, mas depender de "será que esta propriedade existe" é frágil: e se dois formatos, por coincidência, tiverem uma propriedade com o mesmo nome? O padrão que resolve isso de vez, e que você vai ver o tempo todo em código profissional de TypeScript, é a discriminated union (união discriminada): uma união de vários formatos de objeto que compartilham uma propriedade em comum, com um tipo literal diferente em cada um. Essa propriedade compartilhada é o campo discriminante.
O exemplo mais comum do mundo real é exatamente o que qualquer tela que busca dados de um servidor precisa representar: o carregamento ainda não terminou, terminou com sucesso, ou terminou com erro. Compare com a alternativa ingênua de várias variáveis soltas (carregando: boolean, dados: Produto[] | null, erro: string | null), que permite estados sem sentido, como carregando: true e erro: "falhou" ao mesmo tempo:
// Discriminated union: "status" é o campo discriminante, com um valor
// literal diferente em cada variante.
type EstadoPedidos =
| { status: "carregando" }
| { status: "sucesso"; dados: string[] }
| { status: "erro"; erro: string };
function mensagemEstado(estado: EstadoPedidos): string {
switch (estado.status) {
case "carregando":
return "Carregando pedidos...";
case "sucesso":
return estado.dados.length + " pedido(s) encontrado(s)."; // dados só existe aqui
case "erro":
return "Falhou: " + estado.erro; // erro só existe aqui
}
}
console.log(mensagemEstado({ status: "carregando" }));
console.log(mensagemEstado({ status: "sucesso", dados: ["#1", "#2"] }));
console.log(mensagemEstado({ status: "erro", erro: "sem conexão" }));O campo 'status' é o discriminante: cada variante tem um valor literal diferente para ele.
Pense no campo discriminante como a etiqueta de cor numa esteira de bagagens de aeroporto: bagagem com etiqueta vermelha vai para a área de "extraviada", com etiqueta verde vai para a "esteira normal". Você não precisa abrir a mala para saber o que fazer com ela, só olhar a etiqueta. Da mesma forma, o TypeScript não precisa "abrir" o objeto inteiro: só de ver status === "sucesso", ele já sabe, com certeza, que aquele objeto específico tem a propriedade dados, e libera o acesso a ela sem exigir nenhuma checagem extra.
Repare, no exemplo acima, que dentro de case "sucesso": o TypeScript permite estado.dados sem reclamar, mesmo sem nenhuma verificação manual adicional, porque o próprio case já provou, para aquele branch, que estado só pode ser a variante de sucesso. E dentro de case "erro":, tentar acessar estado.dados seria um erro de tipo: aquela variante nem tem essa propriedade.
Há ainda uma vantagem que aparece com o tempo: e se alguém acrescentar um quarto estado à união e esquecer de tratá-lo no switch? Sem proteção, a função devolveria undefined em silêncio. A proteção é o tipo never ("nunca", o tipo que não tem nenhum valor possível): no default, depois de todos os case, o TypeScript já eliminou todas as variantes, então estado tem tipo never. Se sobrar uma variante sem tratamento, ela deixa de ser never e o erro aparece ao escrever, não ao rodar. Isso se chama checagem de exaustividade:
type EstadoTela =
| { status: "carregando" }
| { status: "sucesso"; pedidos: string[] }
| { status: "erro"; mensagem: string };
// Se algum caso ficar sem tratamento, o valor que chega aqui deixa de ser "never"
// e o TypeScript acusa o erro na hora de escrever, não na hora de rodar.
function garantirTodos(valor: never): never {
throw new Error("Caso não tratado: " + JSON.stringify(valor));
}
function descrever(estado: EstadoTela): string {
switch (estado.status) {
case "carregando": return "Carregando...";
case "sucesso": return estado.pedidos.join(", ");
case "erro": return "Erro: " + estado.mensagem;
default: return garantirTodos(estado);
}
}
console.log(descrever({ status: "sucesso", pedidos: ["#1", "#2"] }));
console.log(descrever({ status: "erro", mensagem: "falha na rede" }));
// Experimento: acrescente { status: "vazio" } à união EstadoTela e NÃO crie o case.
// O default passa a dar erro de tipo (TS2345) e te avisa do case que faltou.Experimento: acrescente uma variante { status: "vazio" } à união e veja o default acusar o erro.
Uma união de objetos sem nenhuma propriedade em comum e literal, como { dados: Produto[] } | { mensagem: string }, obriga você a usar in toda vez, e ainda corre o risco de ambiguidade se os dois formatos crescerem com o tempo. Sempre que você estiver modelando "isto pode estar num destes estados diferentes", vale a pena desenhar de propósito um campo discriminante literal (como status, tipo ou kind) desde o início.
EstadoTela já modela os três estados de carregamento de uma lista de pedidos (carregando, sucesso com pedidos: string[], erro com mensagem: string). Complete a função renderizarTela(estado) com um switch no campo status: para "carregando", devolva "Carregando..."; para "sucesso", devolva pedidos.join(", ") (uma string vazia se a lista estiver vazia); para "erro", devolva "Erro: " + mensagem. Feche o switch com um default que atribui estado a uma constante do tipo never e a devolve (checagem de exaustividade): se alguém acrescentar uma variante à união e esquecer de tratá-la, o compilador deve acusar erro.type EstadoTela =
| { status: "carregando" }
| { status: "sucesso"; pedidos: string[] }
| { status: "erro"; mensagem: string };
function renderizarTela(estado: EstadoTela): string {
// sua vez: switch (estado.status), um case para cada variante
// e um default que atribui estado a uma constante do tipo never (exaustividade)
}
console.log(renderizarTela({ status: "carregando" }));
🧩 Este é um exercício com correção automática — abra no app para completá-lo e verificar.