Como tipar a resposta de uma API em TypeScript
O fetch entrega os dados com o tipo any, o que desliga o TypeScript exatamente onde os bugs nascem. Veja como dar um tipo de verdade aos dados da API, com rapidez e segurança.
O TypeScript verifica o seu código, mas não consegue verificar dados que chegam em tempo de execução. No momento em que você chama await response.json(), recebe um valor do tipo any, e a partir daí o compilador deixa você ler user.adress.city, com erro de digitação e tudo, sem reclamar. Tipar as respostas da API fecha essa brecha. Este guia mostra como escrever os tipos rapidamente a partir de um exemplo, como modelar as partes complicadas e quando só um tipo não basta.
Passo 1: tire os tipos de um exemplo real
Escrever interfaces à mão para uma resposta grande é lento e sujeito a erros. Em vez disso, copie uma resposta real, por exemplo da aba Rede (Network) do navegador ou da documentação da API, e cole em JSON para TypeScript. Uma resposta como esta:
{
"id": 1024,
"name": "Ada",
"tags": ["admin", "beta"],
"address": { "city": "London", "zip": "N1 9GU" },
"orders": [
{ "order_id": "A-17", "total": 42, "shipped_at": "2026-09-01T10:00:00Z" },
{ "order_id": "A-18", "total": 18.5, "shipped_at": null, "coupon": "WELCOME" }
]
}
vira isto, com o Nome da raiz definido como User:
export interface Address {
city: string;
zip: string;
}
export interface Order {
order_id: string;
total: number;
shipped_at: string | null;
coupon?: string;
}
export interface User {
id: number;
name: string;
tags: string[];
address: Address;
orders: Order[];
}
Repare no que o gerador deduziu a partir dos dois pedidos: shipped_at era null em um dos itens, então ficou string | null; coupon faltava em um dos itens, então ficou opcional. Os objetos aninhados viraram interfaces próprias com nome, e os itens de orders viraram Order. Tudo roda no navegador, então uma resposta com dados reais de clientes não é enviada a lugar nenhum.
Passo 2: use o tipo com o fetch
O jeito mais simples de ligar o tipo aos dados é uma pequena função auxiliar:
async function getJson<T>(url: string): Promise<T> {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return (await res.json()) as T;
}
const user = await getJson<User>('/api/users/1024');
user.address.city; // string, checked by the compiler
O as T é uma promessa que você faz ao compilador, não uma verificação. Se o servidor mandar outra coisa, o TypeScript não vai perceber. Para APIs internas que você controla, essa troca muitas vezes é aceitável; para qualquer outra coisa, continue até o passo 4.
Passo 3: modele as partes complicadas
| No JSON | No TypeScript | Por quê |
|---|---|---|
| Uma chave que às vezes falta | coupon?: string | Opcional: a propriedade pode não existir |
| Um valor que às vezes é null | shipped_at: string | null | Anulável: a propriedade existe, mas pode estar vazia |
| As duas coisas | note?: string | null | A API pode omitir a chave ou mandar null |
| Uma data | string | O JSON não tem tipo de data; converta com new Date() onde precisar |
| Um ID grande | string se a API o mandar entre aspas | Números acima de 2^53 perdem precisão no JavaScript |
| Um array vazio no exemplo | unknown[] | O exemplo não mostra como são os itens |
Prefira unknown a any para tudo o que o exemplo não consegue informar: unknown obriga a uma verificação antes do uso, enquanto any desliga a verificação em silêncio. Um tipo gerado só é tão completo quanto o seu exemplo, então use uma resposta que inclua os campos opcionais, ou um array com vários itens variados.
Passo 4: valide em tempo de execução quando os dados não são seus
APIs de terceiros mudam, proxies devolvem páginas de erro em HTML e clientes antigos encontram servidores novos. Uma verificação em tempo de execução pega esses casos na fronteira, com um erro claro, em vez de lá no fundo da interface. Duas abordagens comuns:
- JSON Schema: gere um schema a partir do mesmo exemplo com o gerador de JSON Schema e valide com uma biblioteca como o Ajv. O schema é neutro em relação à linguagem, então um backend em outra linguagem pode usar o mesmo arquivo.
- Um validador pensado para TypeScript: bibliotecas como o Zod permitem descrever o formato uma vez e inferir dele o tipo TypeScript, de modo que o tipo e a verificação nunca se desencontram.
Em qualquer caso, valide uma única vez, no ponto onde os dados entram no seu programa, e deixe o resto do código confiar no tipo.
Quando a API muda
Tipos gerados uma vez vão ficando defasados conforme a API cresce. Guarde no repositório, ao lado dos tipos, o exemplo usado para gerá-los, para que a próxima pessoa consiga gerá-los de novo e ver num diff o que mudou. Se a API publica uma descrição OpenAPI, gerar os tipos a partir dela é mais confiável do que a partir de exemplos, porque ela lista cada campo opcional e cada valor possível, inclusive os que uma única resposta nunca mostra. Para APIs sem essa descrição, um validador em tempo de execução como os do passo 4 transforma uma divergência silenciosa num erro que você vê nos logs.
interface ou type?
Para dados de API, os dois funcionam. Interfaces podem ser estendidas e são mescladas quando declaradas duas vezes, o que algumas equipes preferem para tipos públicos; aliases de type também expressam uniões e mapped types. Escolha um estilo para o projeto e mantenha a consistência. O gerador produz os dois.
Outras linguagens a partir do mesmo exemplo
O mesmo exemplo de JSON também pode virar structs em Go, modelos Pydantic ou classes em C# no JSON para Tipos, o que ajuda a manter o frontend e o backend com o mesmo formato. Se o próprio exemplo estiver quebrado, o formatador de JSON mostra onde.