Skip to content
jsonforge.app
Voltar ao blog
Guia7 min de leitura

Gerando tipos TypeScript a partir de JSON: um guia prático

Escrever à mão uma interface TypeScript para uma resposta de API com 40 campos é tedioso e propenso a erros — um typo num nome de campo quebra silenciosamente a segurança de tipos em vez de gerar um erro de compilação. Gerar a interface diretamente de uma amostra JSON real é mais rápido e mais preciso, desde que você entenda onde a inferência precisa adivinhar.

Como funciona a inferência JSON-para-TypeScript

Um gerador percorre a árvore de valores JSON e mapeia cada tipo JavaScript para seu equivalente TypeScript: string continua string, number continua number, boolean continua boolean, null vira null (ou é mesclado numa union), arrays viram T[] onde T é inferido dos elementos, e objetos viram interfaces aninhadas.

Uma amostra JSON e sua interface inferida.
json
{
  "id": 42,
  "email": "ada@example.com",
  "isActive": true,
  "roles": ["admin", "editor"],
  "profile": {
    "displayName": "Ada",
    "avatarUrl": null
  }
}
Saída TypeScript inferida.
typescript
interface Root {
  id: number;
  email: string;
  isActive: boolean;
  roles: string[];
  profile: {
    displayName: string;
    avatarUrl: string | null;
  };
}

Onde a inferência precisa adivinhar

Campos opcionais vs. sempre presentes: uma única amostra JSON não consegue dizer ao gerador se avatarUrl às vezes está totalmente ausente (o que deveria ser avatarUrl?: string | null) ou sempre presente mas às vezes null. Alimente o gerador com algumas amostras representativas, ou marque manualmente um campo como opcional após a geração, se você sabe que a API o omite em certas condições.

Números que são IDs vs. quantidades: o JSON tem um único tipo numérico, mas o TypeScript não consegue distinguir um ID inteiro de um preço de ponto flutuante sem contexto extra. Alguns geradores oferecem branded types ou unions literais para isso; a maioria apenas emite number e deixa a distinção com você.

Arrays vazios: um array sem exemplos dentro ("tags": []) não dá ao gerador nada de onde inferir o tipo do elemento — ele tipicamente recorre a unknown[] ou any[]. Aponte o gerador para uma amostra em que o array esteja populado, ou anote-o manualmente depois.

Unions de literais de string: um campo status que é sempre um de "pending" | "active" | "closed" será inferido como string comum, a menos que o gerador veja todos os valores possíveis na amostra — e mesmo assim, a maioria dos geradores usa por padrão o tipo mais amplo string, a não ser que você ative explicitamente a inferência de union literal.

Datas: o JSON não tem tipo de data — timestamps são sempre strings (ISO 8601) ou números (epoch Unix). Um gerador tipará um campo de data como string ou number, não como Date; você ainda precisa analisá-lo por conta própria após o fetch.

A mesma ideia em outras linguagens

A lógica de inferência — percorrer a árvore, mapear tipos, nomear objetos aninhados — é a mesma independente da linguagem-alvo; apenas a sintaxe de saída muda. Um struct Go recebe o mesmo mapeamento de campos com struct tags para os nomes das chaves JSON; uma dataclass Python recebe type hints e um wrapper opcional Optional[...]; uma classe Java ou C# recebe campos tipados e getters/setters ou records. Se seu time trabalha em múltiplas linguagens contra a mesma API, gerar tipos para cada uma a partir da mesma amostra JSON mantém todas honestas entre si.

A mesma estrutura como um struct Go.
go
type Root struct {
	ID       int     `json:"id"`
	Email    string  `json:"email"`
	IsActive bool    `json:"isActive"`
	Roles    []string `json:"roles"`
	Profile  struct {
		DisplayName string  `json:"displayName"`
		AvatarURL   *string `json:"avatarUrl"`
	} `json:"profile"`
}

Antes de confiar nos tipos gerados

Tipos gerados são um forte ponto de partida, não um contrato final. Antes de commitá-los: confirme os campos opcionais contra a documentação real da API (não apenas uma amostra), substitua campos string vagos que na verdade são enums por unions literais, verifique se campos de array vazio receberam um tipo de elemento real a partir de uma amostra melhor, e gere novamente sempre que a API upstream adicionar ou renomear um campo, em vez de remendar a interface à mão.

FAQ

Um gerador JSON-para-TypeScript consegue lidar com um campo que às vezes é string e às vezes é number?
Sim, se ele vir ambas as variantes nas amostras que recebe — ele deve inferir string | number. Com apenas uma amostra, ele só consegue tipar o que viu. Esse é o principal motivo para gerar a partir de múltiplos payloads representativos, e não de um único exemplo de caminho feliz.
Interfaces geradas devem usar `interface` ou `type`?
Funcionalmente quase idênticos para este caso de uso. `interface` é mais comum para formas de objeto e suporta declaration merging; `type` é mais flexível para unions e intersections. Ambos funcionam para formas de resposta de API — escolha o que corresponde à convenção existente do seu codebase.
Como manter os tipos gerados em sincronia conforme a API evolui?
Trate a geração como uma etapa repetível, não um copy-paste único: mantenha uma amostra real de resposta versionada no repositório (ou obtida de um endpoint de staging) e gere a interface novamente sempre que a amostra mudar, em vez de editar à mão o arquivo gerado. Alguns times ligam isso ao CI para capturar desvios automaticamente.
Isso funciona para JSON profundamente aninhado ou recursivo, como uma estrutura de árvore?
Aninhamento profundo gera interfaces aninhadas sem problema. Estruturas realmente recursivas (um comentário que pode conter respostas da mesma forma) precisam de um tipo autorreferencial, que a maioria dos geradores não infere automaticamente de uma única amostra — tipicamente você precisará escrever à mão a interface recursiva uma vez e reutilizá-la.

Experimente estas ferramentas

Artigos relacionados