Destino Dev

Blog

Validando respostas JSON do fetch com Type Guards genéricos em TypeScript

Por John Pires · 19 de março de 2026

Validando respostas JSON do fetch com Type Guards genéricos em TypeScript

1) O que é e por que importa

Definição prática

Quando você usa fetch() no navegador ou fetch no Node para buscar dados, o que chega ao seu código é um objeto JavaScript — normalmente tipado como any ou unknown em TypeScript. Validar essa resposta significa verificar que o JSON tem a forma esperada antes de tratá-lo como um tipo forte no resto da aplicação.

Por que isso importa

Sem validação você assume que a API sempre retorna o que prometeu. Isso é frágil: mudanças no backend, erros de serialização, ou dados de terceiros quebram código em runtime. Type Guards genéricos permitem centralizar checagens e reaproveitar validação em tempo de execução, mantendo a ergonomia dos tipos TypeScript.

Para iniciantes: pensar em Type Guards como um filtro que transforma um valor de unknown em um tipo seguro, com código explícito que garante a forma do objeto. É a ponte entre o mundo dinâmico do JSON e o sistema de tipos estático do TypeScript.

2) Conceitos fundamentais com analogias

TypeScript x runtime

TypeScript verifica tipos em tempo de compilação — isso não existe no JavaScript que roda no navegador. Uma analogia: o TypeScript é como um manual de montagem; a validação runtime é checar se a peça real tem os furos no lugar antes de apertar os parafusos.

O que é um Type Guard

Um Type Guard é uma função que recebe um valor (geralmente unknown) e retorna um predicado do TypeScript do tipo v is T. Essa assinatura informa ao compilador que, após a chamada, o valor pode ser tratado como T.

Exemplo mental: imagine um detector que aponta "OK" quando um objeto atende o formato de um cartão de crédito. Enquanto o detector não disser "OK", você nunca processa o objeto como cartão de crédito.

3) Passo a passo com codigo comentado

Assinatura de um Type Guard genérico

Primeiro, a definição de um tipo para guards. Isso torna funções reutilizáveis e permite escrever helpers genéricos que aceitam qualquer guard.

Contexto: definimos o tipo de função guard que será usado por nossos validadores e pela função genérica de validação.

// Type guard type: recebe unknown e afirma que é T
export type TypeGuard<T> = (v: unknown) => v is T;

O que acontece: agora podemos anotar funções que declaram "este valor é do tipo T" de forma reutilizável.

Validador genérico simples

Contexto: função que aceita um valor e um guard, e lança erro se o valor não passa na checagem. Útil para centralizar tratamento de erro.

export function validateJson<T>(value: unknown, guard: TypeGuard<T>, name = 'payload'): T {
  if (guard(value)) return value;
  throw new Error(`Invalid ${name} received from server`);
}

O que acontece: ao chamar validateJson<User>(json, isUser), você obtém um User seguro ou um erro controlado.

Guard factory para objetos com propriedades simples

Contexto: muitos objetos são apenas um mapa de propriedades primárias (string, number, boolean). Uma fábrica de guard simplifica criar checadores para esses casos.

type PropCheck = (v: unknown) => boolean;

export function createObjectGuard<T extends Record<string, unknown>>(schema: Record<keyof T, PropCheck>): TypeGuard<T> {
  return (v: unknown): v is T => {
    if (typeof v !== 'object' || v === null) return false;
    const obj = v as Record<string, unknown>;
    for (const key in schema) {
      if (!schema[key](obj[key])) return false;
    }
    return true;
  };
}

O que acontece: cria-se um guard verificando cada chave do schema. Rápido para formas planas e legíveis para iniciantes.

4) Exemplo prático completo

Exemplo: consumindo uma API de usuários

Contexto: vamos modelar um tipo User, criar guard, fazer fetch e validar com nosso wrapper. Esse exemplo é mínimo, funcional e adequado para começar.

// types.ts
export type User = {
  id: number;
  name: string;
  email: string;
  isActive: boolean;
};

// checks.ts
const isNumber = (v: unknown) => typeof v === 'number' && !Number.isNaN(v);
const isString = (v: unknown) => typeof v === 'string';
const isBoolean = (v: unknown) => typeof v === 'boolean';

export const isUser = createObjectGuard<User>({
  id: isNumber,
  name: isString,
  email: isString,
  isActive: isBoolean,
});

O que acontece: definimos User e um guard isUser que valida cada propriedade. Isso evita acessar campos inexistentes mais adiante.

Wrapper de fetch com validação

Contexto: função reutilizável que busca JSON e aplica validação com nossa API genérica.

export async function fetchJson<T>(url: string, guard: TypeGuard<T>): Promise<T> {
  const res = await fetch(url, { headers: { 'Accept': 'application/json' } });
  if (!res.ok) throw new Error(`HTTP error ${res.status}`);
  const json = await res.json();
  return validateJson(json, guard, url);
}

// uso
(async () => {
  try {
    const user = await fetchJson<User>('https://api.exemplo.com/user/1', isUser);
    console.log('Nome do usuário:', user.name);
  } catch (err) {
    console.error('Falha ao obter usuário:', err);
  }
})();

O que acontece: a chamada garante que, se a resposta não tiver a forma esperada, o erro é lançado antes de qualquer uso inseguro dos campos.

Decorator (opcional) para validar retorno de método

Contexto: quem usa decorators pode aplicar validação automático no retorno de funções assíncronas — útil em serviços.

// decorator.ts
export function validateReturn<T>(guard: TypeGuard<T>) {
  return function (target: any, _key: string, descriptor: PropertyDescriptor) {
    const original = descriptor.value;
    descriptor.value = async function (...args: any[]) {
      const result = await original.apply(this, args);
      if (!guard(result)) throw new Error('Invalid return value from decorated method');
      return result;
    };
    return descriptor;
  };
}

// uso em classe
class UserService {
  @validateReturn(isUser)
  async getUser() {
    const resp = await fetch('https://api.exemplo.com/user/1');
    return resp.json(); // será validado pelo decorator
  }
}

O que acontece: o decorator intercepta o retorno e garante a forma antes de propagar para quem chamou o método.

5) Erros comuns, armadilhas e dicas práticas

Erro 1: confiar em any

Usar any esconde problemas. Quando uma API muda, erros surgem em runtime sem pistas no código. Prefira sempre unknown e validação.

Erro 2: tentar fazer guards totalmente genéricos demais

Guards que tentam cobrir todos os casos (arrays aninhados, unions complexas) ficam gigantes e difíceis de manter. Comece com verificações planas e crie helpers para arrays e opcionais.

Lista de dicas práticas:

  • Use unknown como ponto de entrada e converta para tipos específicos com guards.
  • Para arrays, crie um helper isArrayOf que recebe um guard de item.
  • Evite reflexões complexas: mantenha a validação previsível e testável.
Tip: escreva testes unitários para seus guards. Validadores são código crítico — bugs nele quebram toda a app.

Exemplo de helper para arrays e opcionais

Contexto: funções utilitárias simples para evitar repetir lógica quando validar listas ou campos opcionais.

export const isArrayOf = <T>(itemGuard: TypeGuard<T>): PropCheck => (v: unknown) => {
  if (!Array.isArray(v)) return false;
  return v.every(item => itemGuard(item));
};

export const isOptional = (check: PropCheck): PropCheck => (v: unknown) => v === undefined || check(v);

O que acontece: isArrayOf permite validar arrays de itens e isOptional lida com propriedades que podem faltar.

6) Próximos passos para aprofundar

Ferramentas de schema validation

Para projetos maiores, avalie bibliotecas como zod, io-ts ou ajv. Elas trazem geração de schemas, validação, mapeamento de erros e integração com TypeScript. Porém, entender Type Guards antes facilita a migração.

Testes, performance e ergonomia

Guards podem adicionar overhead. Meça quando validar grandes volumes. Em muitos casos valide na entrada (API boundary) e confie internamente. Escreva testes unitários para cada guard e cenários positivos/negativos.

Analise também como integrar validação em pipelines: middleware, services e handlers. Coloque validação na borda do sistema para reduzir checagens repetidas.

Conclusão prática

Type Guards genéricos são uma técnica simples e poderosa para tornar consumo de APIs robusto em TypeScript. Comece definindo guards para suas formas de dados, crie um wrapper fetchJson<T> que usa validateJson, e expanda com helpers (arrays, opcionais) e, se necessário, decorators para padronizar comportamento.

Pequenos investimentos em validação economizam horas de debugging. Aplique essas ideias em pontos de entrada do aplicativo e evolua para soluções de schema quando a complexidade aumentar.

Checklist rápido para aplicar hoje:

  1. Trocar any por unknown nas respostas de fetch.
  2. Implementar TypeGuard<T> e validateJson.
  3. Adicionar guards para os tipos mais usados e testar com unit tests.
  4. Medir impacto e considerar libs de schema quando necessário.

Com essas práticas você terá maior segurança, menos bugs silenciosos e um código TypeScript que reflete verdadeiramente a forma dos dados em runtime.

Assuntos: TypeScript, type-guards, fetch, validation

Leia também