Assertion functions: validando tipos em runtime com tipo
1. O que são Assertion Functions e por que usá-las?
Assertion functions são funções que afirmam uma condição sobre um valor em tempo de execução e, se a condição for verdadeira, refinam o tipo desse valor no escopo do chamador. Diferente de validações tradicionais que retornam um booleano ou simplesmente lançam um erro, as assertion functions têm um efeito duplo: protegem o runtime contra dados inválidos e informam o sistema de tipos do TypeScript sobre o formato real dos dados.
A principal diferença entre validação tradicional e assertion functions está na integração com o type system. Uma função comum que valida um valor pode lançar um erro, mas o TypeScript não saberá que, após a chamada, a variável tem um tipo específico. Já a assertion function comunica essa informação ao compilador, eliminando a necessidade de casts manuais ou verificações redundantes.
Os casos de uso mais comuns incluem validação de entrada de APIs, parsing de dados provenientes de localStorage, desserialização de JSON e qualquer cenário onde dados chegam como unknown e precisam ser garantidos como um tipo específico.
2. Sintaxe e Assinatura de Tipo
A sintaxe de uma assertion function utiliza a palavra-chave asserts na assinatura de tipo. O formato básico é:
function isString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error('Value must be a string');
}
}
Observe que o tipo de retorno declarado é asserts value is string, não boolean. O TypeScript entende que, se a função não lançar uma exceção, o valor passado como argumento deve ser tratado como string a partir daquele ponto. O retorno real da função é void — nunca um booleano.
Exemplo mínimo de uso:
function processInput(input: unknown): void {
isString(input);
// Aqui, input é string
console.log(input.toUpperCase());
}
Se o valor não for uma string, a função lança um erro e o código após a chamada nunca executa. Se passar, o TypeScript sabe que input é string.
3. Combinando com Type Guards e Narrowing
Type guards (com retorno value is Type) e assertion functions (asserts value is Type) são conceitos relacionados, mas com propósitos distintos:
- Type guard: usado em condicionais (
if,while) para criar branching. Retornaboolean. - Assertion function: usada para validação obrigatória. Lança erro se a condição falhar.
Exemplo prático: função assertIsDefined:
function assertIsDefined<T>(value: T | null | undefined): asserts value is T {
if (value === null || value === undefined) {
throw new Error(`Expected value to be defined, but received ${value}`);
}
}
function getUserName(user: { name: string | null }): string {
assertIsDefined(user.name);
// user.name é string aqui
return user.name;
}
Enquanto um type guard exigiria um if para verificar e outro caminho para tratar o caso nulo, a assertion function simplifica o fluxo quando a presença do valor é obrigatória.
4. Assertion Functions com Múltiplas Condições
É possível afirmar múltiplos parâmetros em uma única função usando a sintaxe asserts a is A and b is B:
function assertBothStrings(a: unknown, b: unknown): asserts a is string and b is string {
if (typeof a !== 'string') throw new Error('a must be string');
if (typeof b !== 'string') throw new Error('b must be string');
}
function concatSafe(x: unknown, y: unknown): string {
assertBothStrings(x, y);
return x + y; // Ambos são string
}
Para validação de objetos completos, podemos afirmar que um valor desconhecido é um tipo específico:
interface User {
id: number;
name: string;
email: string;
}
function assertUserData(data: unknown): asserts data is User {
if (typeof data !== 'object' || data === null) {
throw new Error('Data must be an object');
}
const obj = data as Record<string, unknown>;
if (typeof obj.id !== 'number') throw new Error('id must be a number');
if (typeof obj.name !== 'string') throw new Error('name must be a string');
if (typeof obj.email !== 'string') throw new Error('email must be a string');
}
5. Tratamento de Erros e Mensagens Customizadas
O comportamento padrão de uma assertion function é lançar uma exceção quando a condição falha. Mensagens de erro descritivas são essenciais para debug eficiente:
class ValidationError extends Error {
constructor(message: string) {
super(message);
this.name = 'ValidationError';
}
}
function assertPositiveNumber(value: unknown): asserts value is number {
if (typeof value !== 'number') {
throw new ValidationError(`Expected number, received ${typeof value}`);
}
if (value <= 0) {
throw new ValidationError(`Expected positive number, received ${value}`);
}
}
Usar classes de erro customizadas permite capturar erros específicos com instanceof:
try {
assertPositiveNumber(-5);
} catch (error) {
if (error instanceof ValidationError) {
console.error('Validation failed:', error.message);
}
}
6. Limitações e Cuidados ao Usar
-
Escopo limitado: Assertion functions só refinam tipos no escopo onde são chamadas. Não afetam tipos em outros contextos.
-
Não substituem validação completa: Verificam tipos, mas não valores específicos.
asserts value is numbernão garante que o número esteja em um intervalo específico. -
Sincronicidade obrigatória: Assertion functions devem ser síncronas. O TypeScript não suporta
assertsem funções assíncronas porque após umawaito contexto de narrowing se perde. -
Falsos positivos: Se a função não lançar erro, o tipo é considerado verdadeiro mesmo que a validação seja insuficiente. Uma implementação incorreta pode enganar o type system.
function assertAlwaysTrue<T>(value: unknown): asserts value is T {
// Nunca lança erro — perigoso!
}
const x: unknown = 42;
assertAlwaysTrue<string>(x);
// TypeScript acredita que x é string, mas é number — erro em runtime!
7. Exemplo Prático: Validando Payload de API com Desconhecido
interface ApiResponse {
status: 'success' | 'error';
data: {
id: number;
title: string;
completed: boolean;
};
}
function assertApiResponse(data: unknown): asserts data is ApiResponse {
if (typeof data !== 'object' || data === null) {
throw new ValidationError('Response must be an object');
}
const obj = data as Record<string, unknown>;
if (obj.status !== 'success' && obj.status !== 'error') {
throw new ValidationError('status must be "success" or "error"');
}
if (typeof obj.data !== 'object' || obj.data === null) {
throw new ValidationError('data must be an object');
}
const dataObj = obj.data as Record<string, unknown>;
if (typeof dataObj.id !== 'number') throw new ValidationError('data.id must be number');
if (typeof dataObj.title !== 'string') throw new ValidationError('data.title must be string');
if (typeof dataObj.completed !== 'boolean') throw new ValidationError('data.completed must be boolean');
}
async function fetchTodo(id: number): Promise<ApiResponse> {
const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`);
const data: unknown = await response.json();
assertApiResponse(data);
return data; // TypeScript sabe que é ApiResponse
}
8. Comparação com Abordagens Alternativas
Zod e bibliotecas de schema validation oferecem validação mais rica e declarativa, com parsing, transformação e mensagens de erro detalhadas. São recomendadas quando a validação é complexa ou quando o schema precisa ser reutilizado em múltiplos pontos.
Type guards com retorno booleano são mais flexíveis para branching condicional, mas exigem que o chamador trate ambos os caminhos:
function isStringGuard(value: unknown): value is string {
return typeof value === 'string';
}
// Uso com if
if (isStringGuard(x)) {
console.log(x.toUpperCase());
} else {
console.log('Not a string');
}
Assertion functions são ideais quando a validação é obrigatória e o fluxo não deve continuar em caso de falha. Elas funcionam como um "ponto de verificação" que combina segurança em runtime com precisão de tipos.
A escolha entre as abordagens depende do contexto: para validações de entrada críticas, assertion functions oferecem uma ponte elegante entre o mundo incerto do runtime e a precisão do type system do TypeScript.
Referências
- TypeScript Handbook: Assertion Functions — Documentação oficial da Microsoft sobre a feature introduzida no TypeScript 3.7
- TypeScript Deep Dive: Type Guards and Assertion Functions — Guia abrangente com exemplos práticos de type guards e assertion functions
- Assertion Functions in TypeScript — Artigo técnico de Matt Pocock com explicações detalhadas e padrões avançados
- TypeScript 3.7: Assertion Functions — Postagem de Marius Schulz cobrindo sintaxe, casos de uso e limitações
- Using Assertion Functions for Runtime Validation — Tutorial prático no Dev.to mostrando validação de dados de API com assertion functions
- [TypeScript Assertion Functions vs Type Guards](https://www.typescriptlang.org/play?#code/PTAEFkFMBsGMBMECwA4FNIBcCWA7AzgHYAuAlqALygDe0oA7qKALygC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKAO6igC8oA3lKA
A escolha entre as abordagens depende do contexto: para validações de entrada críticas, assertion functions oferecem uma ponte elegante entre o mundo incerto do runtime e a precisão do type system do TypeScript.
Conclusão
As assertion functions representam uma ferramenta poderosa no arsenal do desenvolvedor TypeScript, permitindo que a validação em runtime refine os tipos de forma natural e segura. Diferentemente de type guards que exigem branching condicional, as assertion functions funcionam como barreiras de validação — se o código passa por elas, o tipo é garantido.
O padrão é particularmente útil em:
- Camadas de entrada de aplicações (APIs, formulários, parsing de dados)
- Funções utilitárias que precisam garantir invariantes
- Integrações com sistemas legados ou dados não tipados
No entanto, é importante lembrar que assertion functions não são uma bala de prata. Para validações complexas com schemas aninhados, transformações de dados ou mensagens de erro ricas, bibliotecas como Zod, Yup ou io-ts ainda são recomendadas. As assertion functions brilham quando você precisa de uma validação simples, direta e que se integre perfeitamente com o sistema de tipos.
Ao dominar esse recurso, você ganha a capacidade de escrever código mais seguro e expressivo, reduzindo a distância entre o que o TypeScript sabe em tempo de compilação e o que realmente acontece em tempo de execução.