DEV Community

Yuri Peixinho
Yuri Peixinho

Posted on

Typescript: Refinamento de Tipos (Type Guards / Narrowing)

Introdução

  • Type guards → provam o tipo explicitamente
  • Narrowing → é o processo mais amplo, que inclui type guards e outras formas de afunilar
Narrowing
├── Type Guards
   ├── typeof
   ├── instanceof
   ├── in

├── Equality Narrowing
   ├── === / !==
│   └── == / != (loose, menos comum)
│
└── Truthiness Narrowing
    ├── if (x)
    ├── x && ...
    └── !!x
Enter fullscreen mode Exit fullscreen mode

Type Guards

Um type guard é qualquer expressão que, quando avaliada como true, garante ao TypeScript qual é o tipo dentro daquele bloco.

O problema que eles resolvem

type Resultado = { dados: string[] } | { mensagem: string };

function tratar(r: Resultado) {
  console.log(r.dados); // ❌ erro — TypeScript não sabe qual dos dois é
}
Enter fullscreen mode Exit fullscreen mode

Você precisa provar ao compilador qual tipo está lidando. Type guards fazem isso.

Typeof

O typeof é um operador do próprio JavaScript, o TypeScript apenas o observa e usa o resultado para afunilar o tipo.

O typeof funciona para estruturas primitivas como: string, number, boolean, undefined, symbol, bigint

O que acontece no exemplo passo a passo

function formatar(valor: string | number) {
  // aqui: valor é  string | number

  if (typeof valor === "string") {
    // aqui: TypeScript SABE que é string
    // métodos de string disponíveis ✅
    return valor.toUpperCase();
  }

  // aqui: TypeScript ELIMINOU string da union
  // só sobrou number ✅
  return valor.toFixed(2);
}
Enter fullscreen mode Exit fullscreen mode

Esse "sobrar" é o narrowing acontecendo. O TypeScript rastreia o fluxo e descarta o que foi provado impossível. Por isso o nome traduzido é “estreitamento”, os tipos das variáveis vão se estreitando a medida que o typescript vai elimitando os tipos.

O typeof não distingue objetos entre si:

type NFe  = { chaveAcesso: string };
type NFSe = { codigoVerificacao: string };

function processar(nota: NFe | NFSe) {
  if (typeof nota === "object") {
    // TS ainda não sabe se é NFe ou NFSe ❌
    // typeof não ajuda aqui
  }
}
Enter fullscreen mode Exit fullscreen mode

Para objetos você precisa do in, instanceof, ou um user-defined guard — que são os outros type guards que veremos em seguida.

Instanceof

O instanceof verifica se um objeto foi criado por uma determinada classe — tecnicamente, se o prototype da classe aparece na cadeia de prototypes do objeto.

Sintaxe básica:

objeto instanceof Classe
Enter fullscreen mode Exit fullscreen mode

Retorna true ou false, e o TypeScript usa isso para afunilar o tipo.

Exemplo simples

class ErroDeRede extends Error {
  statusCode: number;
  constructor(msg: string, code: number) {
    super(msg);
    this.statusCode = code;
  }
}

class ErroDeValidacao extends Error {
  campo: string;
  constructor(msg: string, campo: string) {
    super(msg);
    this.campo = campo;
  }
}

function tratar(err: Error) {
  if (err instanceof ErroDeRede) {
    console.log(err.statusCode); // ✅ TypeScript sabe que é ErroDeRede
  }

  if (err instanceof ErroDeValidacao) {
    console.log(err.campo); // ✅ TypeScript sabe que é ErroDeValidacao
  }
}
Enter fullscreen mode Exit fullscreen mode

Passo a passo do Narrowing

function tratar(err: ErroDeRede | ErroDeValidacao) {
  // aqui: err é ErroDeRede | ErroDeValidacao

  if (err instanceof ErroDeRede) {
    // aqui: err é ErroDeRede ✅
    return;
  }

  // aqui: TypeScript eliminou ErroDeRede
  // só sobrou ErroDeValidacao ✅
  console.log(err.campo);
}
Enter fullscreen mode Exit fullscreen mode

Só funciona com classes — não funciona com type ou interface:

type NFe = { chaveAcesso: string };

const nota = { chaveAcesso: "123" };

nota instanceof NFe // ❌ erro de compilação — NFe não é uma classe
Enter fullscreen mode Exit fullscreen mode

Para objetos simples tipados com type ou interface, você usa in ou um user-defined guard com is.

IN

O in verifica se uma propriedade existe em um objeto. É o tipo guard ideal para distinguir entre tipos que são objetos literais (type ou interface), onde instanceof não funciona.

Sintaxe básica

"propriedade" in objeto
Enter fullscreen mode Exit fullscreen mode

Retorna true se a propriedade existe no objeto ou em seu prototype.

Exemplo simples

type Sucesso = { dados: string[] };
type Erro    = { mensagem: string };

function tratar(r: Sucesso | Erro) {
  if ("dados" in r) {
    console.log(r.dados); // ✅ TypeScript sabe que é Sucesso
  } else {
    console.log(r.mensagem); // ✅ TypeScript sabe que é Erro
  }
}
Enter fullscreen mode Exit fullscreen mode

Passo a passo do narrowing

function tratar(r: Sucesso | Erro) {
  // aqui: r é Sucesso | Erro

  if ("dados" in r) {
    // aqui: r é Sucesso ✅
    return;
  }

  // aqui: TypeScript eliminou Sucesso
  // só sobrou Erro ✅
  console.log(r.mensagem);
}
Enter fullscreen mode Exit fullscreen mode

Narrowing

Narrowing é o processo pelo qual o TypeScript reduz um tipo amplo para um mais específico conforme analisa o fluxo do código. Ele não é uma feature isolada — é um comportamento contínuo do compilador.

Alguns desenvolvedores confudem O Type Guard com o Narrow. Podemos dizer que o Type Guard é uma ferramenta que dispara o narrowing

function processar(valor: string | number) {
  // aqui: string | number

  if (typeof valor === "string") {
    // aqui: string
  }

  // aqui: number
}
Enter fullscreen mode Exit fullscreen mode

O TypeScript rastreia o que é possível em cada ponto do código e descarta o que foi provado impossível.

Todas as formas de narrowing juntas

Narrowing
├── typeof           primitivos
├── instanceof       classes
├── in               objetos literais
├── User-defined     lógica customizada (is)
├── Equality         === / !== / == null
├── Truthiness       if (x) / !!x
└── Exhaustiveness   assertNever
Enter fullscreen mode Exit fullscreen mode

, isCada uma é só uma ferramenta diferente para provar ao compilador qual tipo está presente.

O compilador acumula evidências

O narrowing é cumulativo, cada verificação elimina possibilidades:

function processar(valor: string | number | null | undefined) {
  if (valor == null) return;
  // eliminou null e undefined → string | number

  if (typeof valor === "string") return;
  // eliminou string → number

  // aqui: só number ✅
  console.log(valor.toFixed(2));
}
Enter fullscreen mode Exit fullscreen mode

Equality

O TypeScript observa comparações de igualdade e usa o resultado para afunilar o tipo. Existem dois operadores: o estrito (=== / !==) e o loose (== / !=).

=== estrito

Compara valor e tipo — o mais comum:

function processar(x: string | number) {
  if (x === "pendente") {
    // ✅ x é string — e especificamente "pendente"
    return x.toUpperCase();
  }
  // aqui: number
}
Enter fullscreen mode Exit fullscreen mode

!== para exclusão

function processar(valor: string | null) {
  if (valor !== null) {
    // ✅ null foi eliminado — só string
    console.log(valor.toUpperCase());
  }
}
Enter fullscreen mode Exit fullscreen mode

== loose — o único caso útil

Normalmente evitamos == em TypeScript, mas há um caso onde ele é genuinamente melhor: eliminar null e undefined ao mesmo tempo.

function processar(valor: string | null | undefined) {
  // === exigiria duas verificações:
  if (valor !== null && valor !== undefined) { ... }

  // == faz os dois de uma vez:
  if (valor != null) {
    // ✅ null e undefined eliminados — só string
    console.log(valor.toUpperCase());
  }
}
Enter fullscreen mode Exit fullscreen mode

Isso funciona porque null == undefined é true em JavaScript — os dois se cancelam mutuamente com ==.

Truthiness

Truthiness narrowing acontece quando o TypeScript observa se um valor é truthy ou falsy dentro de uma condição e afunila o tipo com base nisso.

Valores falsy em JavaScript

Esses são os únicos valores que avaliam como false num if:

false
0
-0
0n        // bigint zero
""        // string vazia
null
undefined
NaN
Enter fullscreen mode Exit fullscreen mode

Tudo o mais é truthy — incluindo [], {}, "false", -1.

Exemplo básico

function processar(valor: string | null) {
  if (valor) {
    // ✅ null foi eliminado — só string
    console.log(valor.toUpperCase());
  }
}
Enter fullscreen mode Exit fullscreen mode

Type Predicates — guards customizados

Quando nenhum guard nativo basta, você cria o seu com a sintaxe param is Tipo. O TypeScript confia na sua implementação e estreita o tipo em todo lugar que a função for chamada.

// Sintaxe: retorno "param is Tipo" em vez de boolean
function isString(v: unknown): v is string {
  return typeof v === "string";
}

// Caso real: validar shape de objeto externo
type NotaFiscal = { chave: string; valor: number; cnpj: string };

function isNotaFiscal(obj: unknown): obj is NotaFiscal {
  return (
    typeof obj === "object" && obj !== null &&
    "chave" in obj && typeof (obj as any).chave === "string" &&
    "valor" in obj && typeof (obj as any).valor === "number" &&
    "cnpj"  in obj && typeof (obj as any).cnpj  === "string"
  );
}

// Uso — TypeScript estreita o tipo automaticamente
function processarPayload(raw: unknown) {
  if (!isNotaFiscal(raw)) throw new Error("Payload inválido");
  // ✅ raw é NotaFiscal aqui — .chave, .valor, .cnpj disponíveis
  console.log(raw.chave, raw.valor);
}

// Filtrando arrays com predicate (remove undefined do tipo)
const ids: (string | undefined)[] = ["a", undefined, "b"];
const validos: string[] = ids.filter(
  (x): x is string => x !== undefined
); // ✅ tipo inferido como string[]
Enter fullscreen mode Exit fullscreen mode

No fundo, tudo que vimos — typeof, instanceof, in, is, equality, truthiness — são apenas formas diferentes de fornecer evidências para o compilador afunilar o tipo com segurança.

Top comments (0)