Narrowing e discriminated unions: o coração real do TypeScript
- ⬜🧠 TypeScript como mental model: tipos são prova, não anotação(TypeScript Profissional)
Recomendamos completar os pré-requisitos antes de seguir, mas nada te impede de continuar.
Narrowing: estreitando tipo dentro de um bloco
A partir de uma union ampla, condições estreitam o tipo dentro do bloco. Os analisadores são:
- (property check)
- , (equality)
- Truthiness: remove null/undefined/""/0
- User-defined type guards:
Narrowing é o mecanismo central de TS — dominar os 6 acima já resolve ~80% dos casos onde iniciantes recorrem a any.
Discriminated unions: narrowing com tag explícito
| Forma de estreitar | Funciona com | Limite |
|---|---|---|
| Verificação de tipo primitivo | string, número, booleano, função | Não distingue dois objetos diferentes |
| Verificação de instância | Classes | Inútil para objeto literal e para dado vindo de JSON |
| Presença de propriedade | Objetos com campos distintos | Frágil: acrescentar um campo opcional em um dos tipos quebra o estreitamento |
| Campo discriminante literal | Qualquer união de objetos | Exige disciplina ao criar o dado — e é justamente por isso que é confiável |
| Função de guarda de tipo | Casos que o compilador não deduz | A promessa é SUA: se a checagem dentro dela estiver errada, o compilador acredita mesmo assim |
Quando uma union tem variantes com shapes diferentes, use um campo literal pra diferenciar. Esse campo é o discriminant.
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; size: number }
| { kind: 'rect'; w: number; h: number };
function area(s: Shape): number {
switch (s.kind) {
case 'circle': return Math.PI * s.radius ** 2; // s: Circle
case 'square': return s.size ** 2; // s: Square
case 'rect': return s.w * s.h; // s: Rect
default: {
const _exhaustive: never = s; // 🔒 compila só se cobrir tudo
return _exhaustive;
}
}
} Se amanhã você adicionar { kind: 'triangle' } à union, o never do default vira erro de compilação. Você é forçado a atualizar a função. É impossível esquecer — o compilador garante.
Result<T, E>: o padrão Rust em TS
A aplicação mais útil de discriminated union: representar sucesso/erro sem throw.
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
function parseJson<T>(raw: string): Result<T, SyntaxError> {
try { return { ok: true, value: JSON.parse(raw) }; }
catch (e) { return { ok: false, error: e as SyntaxError }; }
}
const r = parseJson<User>(raw);
if (r.ok) console.log(r.value.name); // r.value: User
else console.error(r.error); // r.error: SyntaxErrorVantagem: a assinatura mostra que pode falhar. Chamadores não esquecem de tratar. Throw, ao contrário, é goto tipado fraco — a assinatura mente. Módulo Erros como valores aprofunda isso.
Para que serve uma união com campo discriminante?
`as` é o sinal de que narrowing falhou
Sempre que você escrever value as SomeType, pergunte: o TS tem como provar isso narrowing? Se sim, refatore. as é uma afirmação ao compilador: "confia em mim". Se errar, bug em runtime silencioso — que é justamente o que TS deveria prevenir.
Exceção legítima ao "nunca use any": a fronteira de entrada. JSON.parse devolve any por definição — o caminho correto é tratar o resultado como unknown e validar com Zod antes de usar. Aí o tipo não é uma promessa do programador, está ancorado em validação de runtime.
Perguntas frequentes
❓ O que é estreitamento de tipo?
❓ Para que serve união discriminada?
❓ Por que o compilador reclama mesmo com a verificação feita?
Fixando
Qual técnica garante que todos os casos de uma união foram tratados?
Por que estreitar o tipo por verificação de propriedade é frágil comparado ao campo discriminante?
Terminou de ler?
Marcar como concluído registra o XP, mantém sua sequência e coloca 3 cartas deste módulo na fila de revisão espaçada.
Próximos passos sugeridos
Temas deste módulo
Discussão
Carregando comentários…