JSON para TypeScript: Gere tipos a partir de respostas de API
Se você já chamou uma API REST em TypeScript e tipou a resposta como any, conhece a dor que vem depois. O autocomplete desaparece. Erros de digitação passam despercebidos. Um campo que você achava ser uma string acaba sendo null, e sua aplicação quebra em produção.
A solução? Converter suas respostas JSON em tipos TypeScript adequados. Neste guia, vamos percorrer o mapeamento manual, ferramentas automatizadas e as melhores práticas que mantêm suas integrações de API sólidas como rocha.
Por que tipos TypeScript a partir de JSON são importantes
O sistema de tipos do TypeScript existe para capturar erros antes que eles cheguem aos seus usuários. Quando você modela uma resposta de API com tipos precisos, três coisas acontecem:
- O autocomplete funciona em todo lugar. Seu editor conhece a forma dos dados, então você para de adivinhar nomes de campos.
- Refatorações se tornam seguras. Renomeie uma propriedade e o compilador informa cada lugar que quebra.
- Erros em tempo de execução diminuem. Você trata
null,undefinede formatos inesperados na fronteira, em vez de no fundo da sua aplicação.
Sem tipos, você está basicamente escrevendo JavaScript com passos extras. Com tipos, você obtém um contrato vivo entre seu código e os dados que ele consome.
Mapeamento manual de JSON para TypeScript
Vamos começar com os fundamentos. Dado um objeto JSON, como você o transforma em uma interface TypeScript?
Objetos básicos
Aqui está uma resposta simples de usuário de uma API:
{
"id": 42,
"username": "jdoe",
"email": "jdoe@example.com",
"isActive": true
}
A interface TypeScript correspondente:
interface User {
id: number;
username: string;
email: string;
isActive: boolean;
}
Cada valor JSON mapeia para um tipo primitivo do TypeScript: number, string ou boolean. As chaves do objeto se tornam nomes de propriedades.
Objetos aninhados
APIs reais raramente retornam estruturas planas. Considere um usuário com endereço:
{
"id": 42,
"username": "jdoe",
"address": {
"street": "123 Main St",
"city": "Springfield",
"zipCode": "62704",
"country": "US"
}
}
Separe objetos aninhados em suas próprias interfaces:
interface Address {
street: string;
city: string;
zipCode: string;
country: string;
}
interface User {
id: number;
username: string;
address: Address;
}
Isso mantém cada tipo focado e reutilizável. Se outro endpoint também retorna um Address, você usa a mesma interface.
Arrays
Quando um campo contém um array, você anota o tipo do elemento:
{
"id": 42,
"username": "jdoe",
"roles": ["admin", "editor"],
"posts": [
{ "id": 1, "title": "Hello World" },
{ "id": 2, "title": "TypeScript Tips" }
]
}
interface Post {
id: number;
title: string;
}
interface User {
id: number;
username: string;
roles: string[];
posts: Post[];
}
Use a sintaxe Type[] para casos simples. Para genéricos complexos, Array<Type> pode ser mais claro, mas consistência importa mais do que qual você escolhe.
Campos opcionais
APIs frequentemente retornam campos que podem ou não estar presentes. Marque-os com ?:
// Às vezes a API retorna bio, às vezes não
{
"id": 42,
"username": "jdoe",
"bio": "Software developer"
}
interface User {
id: number;
username: string;
bio?: string; // Pode estar ausente da resposta
}
Um campo opcional (bio?: string) significa que a chave pode estar completamente ausente. Isso é diferente de um campo anulável, onde a chave existe mas o valor pode ser null.
Lidando com casos extremos
APIs em produção são bagunçadas. Veja como lidar com as armadilhas comuns.
Campos anuláveis
Quando um campo pode ser explicitamente null:
{
"id": 42,
"username": "jdoe",
"avatarUrl": null,
"deletedAt": null
}
interface User {
id: number;
username: string;
avatarUrl: string | null;
deletedAt: string | null; // String de data ISO ou null
}
Se um campo pode ser tanto ausente quanto null, combine os dois:
interface User {
id: number;
username: string;
middleName?: string | null; // Pode estar ausente, ou presente mas null
}
Tipos de união
Alguns campos aceitam múltiplos tipos. Uma API de busca pode retornar resultados mistos:
{
"results": [
{ "type": "user", "id": 1, "username": "jdoe" },
{ "type": "post", "id": 2, "title": "Hello" }
]
}
Modele isso com uma união discriminada:
interface UserResult {
type: 'user';
id: number;
username: string;
}
interface PostResult {
type: 'post';
id: number;
title: string;
}
type SearchResult = UserResult | PostResult;
interface SearchResponse {
results: SearchResult[];
}
O campo type atua como discriminante. O TypeScript restringe o tipo automaticamente quando você o verifica:
function renderResult(result: SearchResult) {
if (result.type === 'user') {
console.log(result.username); // TypeScript sabe que é UserResult
} else {
console.log(result.title); // TypeScript sabe que é PostResult
}
}
Enums a partir de valores string
Quando uma API retorna um conjunto fixo de valores string, use uniões de literais string em vez de enums do TypeScript:
{ "status": "active" }
// Valores possíveis: "active", "inactive", "suspended"
type UserStatus = 'active' | 'inactive' | 'suspended';
interface User {
id: number;
status: UserStatus;
}
Uniões de literais string são mais simples, eliminam código morto via tree-shaking e não geram JavaScript em tempo de execução como enum faz. Reserve enum para casos onde você precisa de mapeamento reverso (número para nome).
Interfaces vs aliases de tipo
Tanto interface quanto type podem descrever formas de objetos. Veja quando escolher cada um:
Use interface para formas de objetos que você pode estender:
interface BaseEntity {
id: number;
createdAt: string;
updatedAt: string;
}
interface User extends BaseEntity {
username: string;
email: string;
}
Use type para uniões, interseções e tipos computados:
type ApiResponse<T> = {
data: T;
meta: { total: number; page: number };
};
type UserOrAdmin = User | Admin;
Na prática, ambos funcionam para a maioria dos casos. Escolha uma convenção para seu projeto e mantenha-a. Muitas equipes usam interface para modelos de dados e type para todo o resto.
Usando ferramentas automatizadas
Tipar interfaces manualmente funciona para APIs pequenas, mas se torna tedioso quando você está lidando com endpoints que retornam mais de 50 campos ou estruturas profundamente aninhadas.
Antes de converter JSON em tipos, certifique-se de que seu JSON é válido e bem formatado. Nosso formatador JSON limpa respostas de API bagunçadas, e o validador JSON detecta erros de sintaxe antes que desperdicem seu tempo.
Conversores online
Ferramentas como quicktype, json2ts e conversores JSON-para-TypeScript permitem colar um payload JSON e obter interfaces de volta instantaneamente. Elas lidam com objetos aninhados, arrays e campos opcionais automaticamente.
O fluxo de trabalho típico:
- Chame sua API e copie a resposta
- Cole o JSON no conversor
- Revise os tipos gerados e ajuste a nomenclatura
- Copie o TypeScript para sua base de código
Geração de código a partir de especificações OpenAPI
Se sua API tem uma especificação OpenAPI (Swagger), pule a conversão de JSON completamente. Ferramentas como openapi-typescript geram tipos diretamente a partir da especificação:
npx openapi-typescript https://api.example.com/openapi.json -o ./src/types/api.ts
Essa abordagem é superior porque a especificação define campos opcionais, tipos anuláveis e enums explicitamente. Você obtém tipos precisos sem adivinhar a partir de dados de amostra.
Validação em tempo de execução com Zod
Para máxima segurança, defina suas formas com Zod e infira tipos TypeScript a partir delas:
import { z } from 'zod';
const UserSchema = z.object({
id: z.number(),
username: z.string(),
email: z.string().email(),
isActive: z.boolean(),
bio: z.string().nullable().optional(),
});
type User = z.infer<typeof UserSchema>;
// Agora valide em tempo de execução
const response = await fetch('/api/users/42');
const data = await response.json();
const user = UserSchema.parse(data); // Lança erro se a forma não corresponder
Com Zod, seus tipos e lógica de validação vivem no mesmo lugar. Se a API muda de forma, parse() lança erro imediatamente em vez de deixar dados inválidos se propagarem pela sua aplicação.
Integrando tipos com chamadas de API
Tipos só são úteis se você os aplicar na fronteira onde os dados entram na sua aplicação.
Wrapper de fetch tipado
Crie uma função fetch genérica que garanta os tipos:
async function fetchJson<T>(url: string, schema?: z.ZodType<T>): Promise<T> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
if (schema) {
return schema.parse(data);
}
return data as T;
}
// Uso
const user = await fetchJson<User>('/api/users/42');
const validatedUser = await fetchJson('/api/users/42', UserSchema);
Axios com tipos
Se você usa Axios, aplique tipos ao genérico da resposta:
import axios from 'axios';
interface PaginatedResponse<T> {
data: T[];
meta: {
total: number;
page: number;
perPage: number;
lastPage: number;
};
}
async function getUsers(page: number = 1) {
const response = await axios.get<PaginatedResponse<User>>('/api/users', {
params: { page },
});
return response.data; // Completamente tipado
}
React Query / TanStack Query
Em aplicações React, combine tipos com sua biblioteca de busca de dados:
import { useQuery } from '@tanstack/react-query';
function useUser(id: number) {
return useQuery({
queryKey: ['user', id],
queryFn: () => fetchJson<User>(`/api/users/${id}`),
});
}
// No seu componente
const { data: user, isLoading } = useUser(42);
// user é tipado como User | undefined
Melhores práticas
Habilite verificações estritas de null
No seu tsconfig.json, habilite o modo strict (ou no mínimo strictNullChecks). Isso obriga você a tratar null e undefined explicitamente:
{
"compilerOptions": {
"strict": true
}
}
Sem verificações estritas de null, o TypeScript permite acessar .username em um valor potencialmente null sem reclamar. Isso anula o propósito de tipar suas respostas de API.
Use readonly para dados imutáveis
Respostas de API são dados que você recebeu, não dados que deveria mutar. Marque propriedades como readonly:
interface User {
readonly id: number;
readonly username: string;
readonly email: string;
readonly createdAt: string;
}
Ou use um tipo utilitário para a interface inteira:
type ImmutableUser = Readonly<User>;
Isso previne mutações acidentais que causam bugs sutis.
Tipos nominais para IDs
Previna a confusão de IDs de diferentes entidades com tipos nominais:
type UserId = number & { readonly __brand: 'UserId' };
type PostId = number & { readonly __brand: 'PostId' };
function getUser(id: UserId): Promise<User> { /* ... */ }
function getPost(id: PostId): Promise<Post> { /* ... */ }
const userId = 42 as UserId;
const postId = 7 as PostId;
getUser(userId); // OK
getUser(postId); // Erro de tipo! Não pode passar PostId onde UserId é esperado
Centralize seus tipos
Mantenha todos os tipos relacionados à API em um diretório dedicado:
src/
types/
api/
user.ts
post.ts
common.ts // Tipos compartilhados como PaginatedResponse
index.ts // Re-exportações
Isso torna os tipos fáceis de encontrar, importar e manter. Quando uma API muda, você atualiza um arquivo e o compilador mostra tudo que quebra.
Valide na fronteira
Nunca confie em dados externos. Mesmo com tipos perfeitos, a API pode retornar algo inesperado. Valide as respostas onde elas entram na sua aplicação:
// Não faça isso
const user = (await response.json()) as User; // Confia cegamente
// Faça isso
const data = await response.json();
const user = UserSchema.parse(data); // Valida a forma
Asserções de tipo (as User) silenciam o compilador mas não verificam os dados. Validação em tempo de execução (Zod, io-ts, Valibot) realmente verifica se a forma corresponde.
Trabalhando com estruturas JSON complexas
Ao lidar com payloads JSON grandes ou complexos, use o editor JSON para explorar e entender a estrutura antes de escrever tipos. É muito mais fácil ver relacionamentos aninhados em um editor adequado do que em texto bruto.
Para equipes que trabalham com APIs JSON, confira estes guias relacionados:
- Melhores Práticas de Formatação JSON — mantenha seu JSON limpo e legível
- Guia de Validação de JSON Schema — valide contratos de API com JSON Schema
- Padrões de Design de API JSON — projete APIs que são fáceis de tipar
- Como Validar JSON — detecte payloads malformados cedo
Juntando tudo
Aqui está um exemplo completo que une tudo — do JSON bruto aos dados validados e tipados em um componente React:
import { z } from 'zod';
// 1. Defina o schema (fonte única da verdade)
const UserSchema = z.object({
id: z.number(),
username: z.string(),
email: z.string().email(),
isActive: z.boolean(),
role: z.enum(['admin', 'editor', 'viewer']),
profile: z.object({
bio: z.string().nullable(),
avatarUrl: z.string().url().nullable(),
socialLinks: z.array(z.object({
platform: z.string(),
url: z.string().url(),
})).optional(),
}),
});
// 2. Infira o tipo a partir do schema
type User = z.infer<typeof UserSchema>;
// 3. Crie uma função fetch tipada
async function getUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error(`Failed to fetch user ${id}`);
const data = await response.json();
return UserSchema.parse(data);
}
// 4. Use na sua aplicação com total segurança de tipos
const user = await getUser(42);
console.log(user.profile.bio); // string | null — o compilador sabe
console.log(user.profile.socialLinks); // array ou undefined — o compilador sabe
Sem any. Sem adivinhação. Sem surpresas em tempo de execução.
Conclusão
Converter JSON para tipos TypeScript não é apenas sobre fazer o compilador feliz. É sobre construir uma rede de segurança que captura erros cedo, potencializa o autocomplete do seu editor e torna sua base de código mais fácil de manter.
Comece simples: copie uma resposta JSON, escreva a interface manualmente e tipe suas chamadas fetch. Conforme seu projeto cresce, adote Zod para validação em tempo de execução e geração de código OpenAPI para APIs grandes. O investimento inicial se paga na primeira vez que um erro de tipo captura um bug que teria chegado à produção.