Pular para o conteúdo
TypeScript

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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.