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 a segurança de tipos sem nenhum erro de compilação. Gerar a interface a partir 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 elementos ("tags": []) não dá ao gerador nenhuma pista do tipo do elemento, então ele recorre a unknown[] ou any[]. Aponte o gerador para uma amostra em que o array tenha itens, ou anote-o à mão depois.

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

Datas: o JSON não tem tipo de data, então timestamps chegam como strings (ISO 8601) ou números (epoch Unix). Um gerador tipa um campo de data como string ou number, não como Date, e você ainda precisa analisá-lo depois do fetch.

A mesma ideia em outras linguagens

A lógica de inferência (percorrer a árvore, mapear tipos, nomear objetos aninhados) é a mesma para qualquer linguagem-alvo; só 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 Optional[...] quando necessário; uma classe Java ou C# recebe campos tipados e getters/setters ou records. Se seu time trabalha em várias linguagens contra a mesma API, gere os tipos de cada uma a partir da mesma amostra JSON para que fiquem coerentes.

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

Trate os tipos gerados como ponto de partida e termine o contrato você mesmo. Antes de fazer commit, confirme os campos opcionais com a documentação da API (uma amostra não basta), troque por unions literais os campos string vagos que guardam enums, verifique se campos de array vazio receberam um tipo de elemento real a partir de uma amostra melhor, e gere de novo 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 as duas variantes entre as amostras que você fornece; ele deve inferir string | number. Com uma amostra só, ele consegue tipar apenas o que viu. Por isso vale gerar a partir de vários payloads representativos, e não de um único exemplo de caminho feliz.
Interfaces geradas devem usar `interface` ou `type`?
Para este caso eles funcionam quase igual. `interface` é mais comum para formas de objeto e suporta declaration merging; `type` lida melhor com unions e intersections. Os dois servem para formas de resposta de API, então siga a convenção 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 recursivas (um comentário que contém respostas da mesma forma) precisam de um tipo autorreferencial, e a maioria dos geradores não o infere de uma única amostra. Escreva a interface recursiva à mão uma vez e reutilize.

Experimente estas ferramentas

Artigos relacionados