Type Guards no TypeScript: Guia Prático
Domine type guards no TypeScript. De typeof e instanceof até user-defined guards com is, tudo explicado com exemplos que você aplica no próximo PR.
O Que São Type Guards no TypeScript
Type guard é qualquer expressão que estreita (narrow) o tipo de uma variável dentro de um bloco de código. Quando o TypeScript vê um if (typeof x === 'string'), ele sabe que dentro daquele if, x é string. Isso é narrowing.
O TypeScript já vem com type guards nativos: typeof, instanceof e o operador in. Mas o mais poderoso são os user-defined type guards, onde você cria suas próprias funções de verificação usando a keyword is.
A ideia central é: você recebe um tipo amplo (como unknown ou uma union) e escreve lógica que comprova pro compilador qual subtipo ele é. A partir daí, o TypeScript confia em você e libera acesso a propriedades e métodos específicos daquele subtipo.
Sem type guards, você acaba usando as pra forçar tipos. E as não faz verificação nenhuma em runtime. Se o tipo tiver errado, seu código quebra silenciosamente. Type guards resolvem isso fazendo a checagem de fato.
Como Criar Type Guards Passo a Passo
Do mais simples ao mais sofisticado, cada técnica de narrowing tem seu lugar. Vamos percorrer todas.
- Passo 1 - typeof para tipos primitivos: Use typeof pra verificar string, number, boolean, symbol, undefined, bigint e function. É o guard mais básico e mais usado. Funciona só com primitivos.
- Passo 2 - instanceof para classes: Quando trabalha com classes, instanceof verifica se o objeto é instância daquela classe. Funciona com herança também: o TypeScript estreita pro tipo específico da classe verificada.
- Passo 3 - Operador in para propriedades: O in checa se uma propriedade existe no objeto. Se 'email' in user, o TypeScript sabe que user tem email. Útil quando tipos diferentes têm propriedades exclusivas.
- Passo 4 - User-defined type guard com is: Crie funções que retornam value is Type. Dentro da função, faça a verificação que quiser. Se retornar true, o TypeScript assume que o valor é do tipo declarado.
- Passo 5 - Combine guards com discriminated unions: Quando a union tem uma propriedade literal comum (type: 'success' | type: 'error'), use switch/case pra estreitar automaticamente. O TypeScript entende cada case.
- Passo 6 - Assertion functions com asserts: Em vez de retornar boolean, você pode criar funções que lançam erro quando o tipo tá errado. Use asserts value is Type. O TypeScript estreita o tipo depois da chamada.
Exemplos Práticos de Type Guards
Vamos ver cada tipo de guard funcionando em cenários que você encontra no dia a dia.
typeof: Narrowing de Primitivos
function formatar(valor: string | number | boolean): string {
if (typeof valor === "string") {
// TypeScript sabe: valor é string aqui
return valor.toUpperCase();
}
if (typeof valor === "number") {
// TypeScript sabe: valor é number aqui
return valor.toFixed(2);
}
// TypeScript sabe: valor é boolean aqui
return valor ? "Sim" : "Não";
}
console.log(formatar("hello")); // "HELLO"
console.log(formatar(3.14159)); // "3.14"
console.log(formatar(true)); // "Sim"
instanceof: Narrowing de Classes
class HttpError {
constructor(public status: number, public message: string) {}
}
class ValidationError {
constructor(public field: string, public reason: string) {}
}
function tratarErro(erro: HttpError | ValidationError) {
if (erro instanceof HttpError) {
// TypeScript sabe: erro é HttpError
console.log(`HTTP ${erro.status}: ${erro.message}`);
} else {
// TypeScript sabe: erro é ValidationError
console.log(`Campo ${erro.field}: ${erro.reason}`);
}
}
tratarErro(new HttpError(404, "Not Found"));
// "HTTP 404: Not Found"
tratarErro(new ValidationError("email", "Formato inválido"));
// "Campo email: Formato inválido"
Operador in: Verificar Propriedades
interface Carro {
marca: string;
portas: number;
}
interface Moto {
marca: string;
cilindradas: number;
}
function descrever(veiculo: Carro | Moto): string {
if ("portas" in veiculo) {
// TypeScript sabe: veiculo é Carro
return `${veiculo.marca} com ${veiculo.portas} portas`;
}
// TypeScript sabe: veiculo é Moto
return `${veiculo.marca} com ${veiculo.cilindradas}cc`;
}
console.log(descrever({ marca: "Honda", portas: 4 }));
// "Honda com 4 portas"
console.log(descrever({ marca: "Yamaha", cilindradas: 600 }));
// "Yamaha com 600cc"
User-Defined Type Guard com is
interface User {
id: number;
name: string;
email: string;
}
interface Admin extends User {
permissions: string[];
level: number;
}
// Type guard customizado com is
function isAdmin(user: User): user is Admin {
return "permissions" in user && "level" in user;
}
function exibirInfo(user: User) {
console.log(`Nome: ${user.name}`);
if (isAdmin(user)) {
// TypeScript sabe: user é Admin aqui
console.log(`Level: ${user.level}`);
console.log(`Permissões: ${user.permissions.join(", ")}`);
}
}
// Filtrando arrays com type guard
const usuarios: User[] = [
{ id: 1, name: "João", email: "joao@mail.com" },
{ id: 2, name: "Ana", email: "ana@mail.com",
permissions: ["read", "write"], level: 2 } as Admin,
];
// admins tem tipo Admin[] automaticamente!
const admins = usuarios.filter(isAdmin);
Assertion Functions com asserts
// Assertion function: lança erro se falhar
function assertString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new Error(`Expected string, got ${typeof value}`);
}
}
function assertNotNull<T>(value: T | null | undefined): asserts value is T {
if (value === null || value === undefined) {
throw new Error("Value is null or undefined");
}
}
// Uso prático
function processarDado(input: unknown) {
assertString(input);
// A partir daqui, input é string
console.log(input.toUpperCase());
}
function buscarUsuario(id: number) {
const user = db.find(u => u.id === id); // User | undefined
assertNotNull(user);
// A partir daqui, user é User
console.log(user.name);
}
Cada técnica tem seu espaço. typeof pra primitivos, instanceof pra classes, in pra checar propriedades, is pra lógica customizada e asserts pra validações que devem travar a execução. Use a ferramenta certa pro contexto certo.
Erros Comuns com Type Guards
Armadilhas que sabotam seu narrowing
Type guard com is que não verifica de verdade: se sua função isAdmin retorna true sem checar as propriedades, o TypeScript confia em você cegamente. Se o tipo tiver errado, o bug vai explodir em runtime. Sempre valide de fato.
typeof com null retorna 'object': essa é clássica. typeof null === 'object' é true em JavaScript. Sempre cheque null separadamente antes de usar typeof pra objetos.
instanceof não funciona com interfaces: interfaces não existem em runtime. Você não pode fazer value instanceof MinhaInterface. Use o operador in ou crie um user-defined type guard.
Narrowing que não persiste em callbacks: dentro de um if (typeof x === 'string'), se você passar x pra um callback assíncrono, o narrowing pode não ser preservado. Guarde o valor numa variável local.
Assertion function sem throw: se a assertion function não lança erro no caso negativo, o TypeScript estreita o tipo mesmo assim. Seu código parece seguro, mas não é. Sempre lance erro no caminho infeliz.
Checklist de Type Guards
- typeof pra primitivos (string, number, boolean)
- instanceof pra instâncias de classes
- Operador in pra verificar existência de propriedades
- User-defined guards (is) com verificação real no corpo da função
- Assertion functions (asserts) lançam erro no caso inválido
- Não usa as pra forçar tipos onde um guard resolve
- Trata typeof null === 'object' separadamente
- Type guards usados em .filter() retornam array tipada corretamente
TypeScript Avançado na Prática
Type guards são o alicerce de código TypeScript seguro e legível. Mas o poder de verdade aparece quando você combina narrowing com generics, discriminated unions e conditional types num projeto completo. No CrazyStack, você constrói um SaaS inteiro com TypeScript, Node.js e React, aplicando esses padrões em cenários reais de produção.
Se você quer parar de lutar contra o compilador e começar a deixar ele trabalhar a seu favor, esse é o próximo passo.
Perguntas frequentes
O Que São Type Guards no TypeScript
Type guard é qualquer expressão que estreita (narrow) o tipo de uma variável dentro de um bloco de código. Quando o TypeScript vê um if (typeof x === 'string'), ele sabe que dentro daquele if, x é string. Isso é narrowing. O TypeScript já vem com type guards nativos: typeof, instanceof e o operador in. Mas o mais poderoso são os user-defined type guards, onde você cria suas próprias funções de verificação usando a keyword is. A ideia central é: você recebe um tipo amplo (como unknown ou uma union) e escreve lógica que comprova pro compilador qual subtipo ele é. A partir daí, o TypeScript confia em você e libera acesso a propriedades e métodos específicos daquele subtipo.
Como Criar Type Guards Passo a Passo
Do mais simples ao mais sofisticado, cada técnica de narrowing tem seu lugar. Vamos percorrer todas.