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 PaidOrdersignifica: se a função retornatrueentãoordertem tipoPaidOrder.- Em tempo de execução, a função ainda retorna apenas
true/false. A parteissó existe em tempo de compilação. - No ramo
false, TypeScript removePaidOrderda union.ordertorna-sePendingOrder | 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
truedeve significar “pertence ao tipo T”. Retornarfalsedeve significar “não pertence ao tipo T”. Não coloque lógica de negócio (como> 1000) em type guards. Separe-as em funçõesbooleanseparadas.
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ãoreturnnenhum valor, ou seja, a função retornavoid. - 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 tipoPromise<asserts x is T>não existe. Se você precisar fazer verificação assíncrona,awaitpara 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
isCustomerpara 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 quevalue 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.