Type guard e assertion function no TypeScript

1. Visão Geral

TypeScript faz narrowing (redução de tipos) muito bem com typeof, instanceof ou ===. Quando a lógica de verificação fica mais complexa e você a extrai em uma função separada, o compilador não consegue mais “enxergar” o resultado da verificação.

Este artigo apresenta duas ferramentas que resolvem esse problema. São type guard (value is T) e assertion function (asserts value is T). Depois de ler, você saberá como escrevê-las, verificar dados unknown vindos de APIs, escrever testes para elas e identificar os casos em que elas silenciosamente enganam o compilador.

2. Narrowing integrado e suas limitações

Usaremos um domínio de pedidos como exemplo ao longo do artigo:

type PendingOrder = { status: "pending"; id: string; total: number };
type PaidOrder = { status: "paid"; id: string; total: number; paidAt: Date };
type CancelledOrder = { status: "cancelled"; id: string; reason: string };

type Order = PendingOrder | PaidOrder | CancelledOrder;

function describe(order: Order): string {
  if (order.status === "paid") {
    return `Pedido ${order.id} foi pago em ${order.paidAt.toISOString()}`;
  }
  return `Pedido ${order.id} está no status ${order.status}`;
}

Order é uma discriminated union com status como campo discriminador. Dentro do ramo if, TypeScript sabe que order é PaidOrder, então acessar paidAt é válido.

Agora você quer reutilizar essa condição e extraí-la em uma função:

function isPaid(order: Order): boolean {
  return order.status === "paid";
}

function describe(order: Order): string {
  if (isPaid(order)) {
    return `Pago em ${order.paidAt.toISOString()}`;
    //                            ^^^^^^ erro
  }
  return "Não pago";
}
error TS2339: Property 'paidAt' does not exist on type 'Order'.
  Property 'paidAt' does not exist on type 'PendingOrder'.

A função isPaid retorna boolean. O tipo boolean não traz informação nenhuma sobre order, então o compilador não consegue fazer narrowing. Type guard foi criado para resolver exatamente isso.

3. Type guard definido pelo usuário

3.1. Sintaxe value is T

Você só precisa mudar o tipo de retorno de boolean para um type predicate:

function isPaidOrder(order: Order): order is PaidOrder {
  return order.status === "paid";
}

function describe(order: Order): string {
  if (isPaidOrder(order)) {
    return `Pago em ${order.paidAt.toISOString()}`;
  }
  // aqui order: PendingOrder | CancelledOrder
  return `Status: ${order.status}`;
}

console.log(describe({ status: "paid", id: "A1", total: 120, paidAt: new Date("2026-10-01") }));
console.log(describe({ status: "cancelled", id: "A2", reason: "Fora de estoque" }));
Pago em 2026-10-01T00:00:00.000Z
Status: cancelled

Explicação:

  • order is PaidOrder significa: se a função retorna true então order tem tipo PaidOrder.
  • Em tempo de execução, a função ainda retorna apenas true/false. A parte is só existe em tempo de compilação.
  • No ramo false, TypeScript remove PaidOrder da union. order torna-se PendingOrder | CancelledOrder.
  • O nome do parâmetro no predicado (order) deve corresponder a um parâmetro da função.

3.2. Filtrando arrays

Type guard é especialmente útil com Array.prototype.filter, porque filter tem overloads que aceitam predicados:

const orders: Order[] = [
  { status: "pending", id: "A1", total: 50 },
  { status: "paid", id: "A2", total: 120, paidAt: new Date() },
  { status: "paid", id: "A3", total: 80, paidAt: new Date() },
];

const paidOrders = orders.filter(isPaidOrder); // PaidOrder[]
const revenue = paidOrders.reduce((sum, o) => sum + o.total, 0);

console.log(revenue);
200

paidOrders tem tipo PaidOrder[] e não Order[]. Assim, qualquer operação depois consegue acessar paidAt sem precisar fazer type casting.

A partir do TypeScript 5.5, o compilador infere automaticamente type predicates para arrow functions simples. Por exemplo, orders.filter(o => o.status === "paid") também retorna PaidOrder[]. Essa inferência só funciona quando a expressão é simples o suficiente e a relação “true ⇔ pertence ao tipo” é precisa. Com lógica mais complexa, você ainda precisa escrever o type guard explicitamente.

3.3. Verificando dados unknown

O caso de uso mais prático de type guard é verificar dados externos, como JSON de uma API:

interface Customer {
  id: string;
  name: string;
  email: string;
}

function isCustomer(value: unknown): value is Customer {
  return (
    typeof value === "object" &&
    value !== null &&
    "id" in value && typeof value.id === "string" &&
    "name" in value && typeof value.name === "string" &&
    "email" in value && typeof value.email === "string"
  );
}

const raw: unknown = JSON.parse('{"id":"C1","name":"Lan","email":"lan@example.com"}');

if (isCustomer(raw)) {
  console.log(raw.email.toUpperCase());
}
LAN@EXAMPLE.COM

A partir do TypeScript 4.9, o operador in reduz value para object & Record<"id", unknown>. Portanto você consegue escrever value.id sem precisar de as any. Cada propriedade é verificada tanto sua existência quanto seu tipo.

3.4. Cilada: type guard mentindo

O compilador confia absolutamente em type predicates. Ele não verifica se o corpo da função realmente corresponde à promessa:

function isCustomerBroken(value: unknown): value is Customer {
  return typeof value === "object" && value !== null; // esqueceu de verificar as propriedades
}

const data: unknown = { id: 1 };
if (isCustomerBroken(data)) {
  console.log(data.email.toUpperCase()); // compila OK, explode em runtime
}
TypeError: Cannot read properties of undefined (reading 'toUpperCase')

Há uma cilada mais sutil localizada no ramo false:

function isLargeAmount(value: string | number): value is number {
  return typeof value === "number" && value > 1000;
}

function format(value: string | number) {
  if (isLargeAmount(value)) return `Grande: ${value}`;
  return value.toUpperCase(); // TS pensa que value: string
}

format(5); // TypeError: value.toUpperCase is not a function

A função retorna false para o número 5. TypeScript então infere que no ramo restante value não é number. Isso está errado.

Observação: Type predicate é uma relação bidirecional. Retornar true deve significar “pertence ao tipo T”. Retornar false deve significar “não pertence ao tipo T”. Não coloque lógica de negócio (como > 1000) em type guards. Separe-as em funções boolean separadas.

4. Assertion function

4.1. asserts value is T

Type guard força você a envolver o código em um if. Há momentos em que você quer um comportamento tipo “se errado, lance erro; caso contrário, continue”. Esse é o trabalho de uma assertion function:

function assertIsCustomer(value: unknown): asserts value is Customer {
  if (!isCustomer(value)) {
    throw new TypeError("Dados não são um Customer válido");
  }
}

function greet(payload: unknown): string {
  assertIsCustomer(payload);
  // a partir deste ponto, payload: Customer
  return `Olá ${payload.name}`;
}

console.log(greet({ id: "C1", name: "Minh", email: "minh@example.com" }));
console.log(greet({ id: "C2" }));
Olá Minh
TypeError: Dados não são um Customer válido

Diferenças em relação a type guard:

  • O tipo de retorno é asserts value is Customer. O corpo da função não return nenhum valor, ou seja, a função retorna void.
  • A função lança um erro quando a condição é falsa. Se a função retorna normalmente, TypeScript assume que a condição é verdadeira.
  • O tipo é reduzido para todo o código após a chamada, não apenas dentro de um bloco if.

4.2. asserts condition

A segunda forma não está vinculada a um tipo específico. Afirma que uma expressão é truthy, como assert do Node.js:

function assert(condition: unknown, message: string): asserts condition {
  if (!condition) {
    throw new Error(message);
  }
}

function applyDiscount(order: Order, percent: number): number {
  assert(order.status !== "cancelled", `Pedido ${order.id} foi cancelado`);
  // order: PendingOrder | PaidOrder → tem total
  assert(percent > 0 && percent <= 50, "Desconto deve estar em (0, 50]");
  return order.total * (1 - percent / 100);
}

console.log(applyDiscount({ status: "pending", id: "A1", total: 200 }, 10));
180

Após a linha assert(order.status !== "cancelled", ...), TypeScript reduz order como se você tivesse acabado de escrever if. Portanto order.total é válido, mesmo que CancelledOrder não tenha a propriedade total.

4.3. Erro TS2775: faltando anotação de tipo explícita

Este é o erro mais comum ao escrever assertion functions como arrow functions:

const assertPositive = (n: number): asserts n => {
  if (n <= 0) throw new Error("Deve ser positivo");
};

assertPositive(5);
error TS2775: Assertions require every name in the call target
to be declared with an explicit type annotation.

O compilador precisa saber o tipo de assertPositive no ponto de declaração, não pode se basear em inferência. Você tem duas formas de corrigir. A primeira é usar uma declaração function. A segunda é anotar o tipo da variável:

type AssertPositive = (n: number) => asserts n;

const assertPositive: AssertPositive = (n) => {
  if (n <= 0) throw new Error("Deve ser positivo");
};

Esta regra também se aplica a métodos chamados através de objetos, por exemplo validator.assertCustomer(x). A variável validator deve ter tipo declarado explicitamente, por exemplo const validator: CustomerValidator = new CustomerValidator().

Observação: Assertion functions não podem ser async. O tipo Promise<asserts x is T> não existe. Se você precisar fazer verificação assíncrona, await para obter os dados primeiro, depois chame a assertion function síncrona no resultado.

5. Escrevendo testes para type guards e assertion functions

Isso é lógica de tempo de execução, então você precisa testar como qualquer outra função. O exemplo abaixo usa Vitest:

npm install -D vitest typescript
// customer.test.ts
import { describe, it, expect } from "vitest";
import { isCustomer, assertIsCustomer } from "./customer";

describe("isCustomer", () => {
  it("retorna true para objeto válido", () => {
    expect(isCustomer({ id: "C1", name: "Lan", email: "lan@example.com" })).toBe(true);
  });

  it.each([
    null,
    "C1",
    { id: "C1", name: "Lan" },
    { id: 1, name: "Lan", email: "lan@example.com" },
  ])("retorna false para %j", (input) => {
    expect(isCustomer(input)).toBe(false);
  });
});

describe("assertIsCustomer", () => {
  it("lança TypeError quando dados são inválidos", () => {
    expect(() => assertIsCustomer({ id: "C1" })).toThrow(TypeError);
  });

  it("reduz o tipo quando dados são válidos", () => {
    const payload: unknown = { id: "C1", name: "Lan", email: "lan@example.com" };
    assertIsCustomer(payload);
    expect(payload.email).toBe("lan@example.com"); // compila graças ao narrowing
  });
});
npx vitest run
 ✓ customer.test.ts (7 tests)
 Test Files  1 passed (1)
      Tests  7 passed (7)

Preste atenção aos casos negativos: faltando propriedade, tipo errado, null, valores primitivos. A maioria dos bugs em type guards está nos casos onde retorna true quando deveria ser false, como no exemplo da seção 3.4. O último teste também funciona como teste em tempo de compilação. Se alguém mudar a assinatura da função para void, o arquivo de teste não vai compilar.

6. Escolhendo entre type guard e assertion function

Situação Deve usar
Dados incorretos são normais, precisa ramificar para tratar Type guard
Filtrar arrays, find, combinar com filter Type guard
Dados incorretos é erro de programação ou violação de contrato, deve parar Assertion function
Verificar no início da função (precondição), evitar if aninhado Assertion function

As duas ferramentas geralmente trabalham juntas. Você escreve um type guard como núcleo, depois o envolve em uma assertion function, como assertIsCustomer chama isCustomer. Assim a lógica de verificação fica em um único lugar.

Quando NÃO escrever próprio:

  • Schema grande ou profundamente aninhado. Escrever manualmente isCustomer para um objeto com 30 propriedades e arrays aninhados é muito fácil de cometer erros. Uma biblioteca de validação como Zod ou Valibot gera tipos e validação a partir do mesmo schema, então os dois não podem divergir.
  • Substituir as. Type guard com corpo vazio (return true) não é mais seguro que value as Customer. Apenas mascara o problema de forma mais sutil.
  • Discriminated union simples. Se if (order.status === "paid") já é claro o suficiente, não extraia em uma função só para ter uma.

7. Conclusão

Type guard (value is T) permite que você encapsule a lógica de verificação de tipo em uma função e ainda mantenha o narrowing no local de chamada. Funciona especialmente bem com if e filter. Assertion function (asserts value is T ou asserts condition) reduz o tipo para todo o código após lançando erro quando a condição é falsa. Funciona bem para verificar precondições e dados de entrada.

Ambas são promessas que o compilador não verifica. Portanto, lembre-se de três coisas:

  • Mantenha a relação “true ⇔ pertence ao tipo”.
  • Declare tipo explicitamente para assertion functions.
  • Teste bem os casos negativos.
typescripttype guardassertion functionnarrowingtype safety