JSON Toolkit

JSON para TypeScript: interfaces, type guards e os limites da geração

Resumo: Gerar uma interface a partir de um JSON de amostra é metade do trabalho: campos opcionais, null, unions, datas e inteiros grandes precisam de uma passada humana, e os tipos não fazem validação em tempo de execução.

Intermediário~9 min de leituraPublicado em 2026-10-31Atualizado em 2026-11-04
Capa do guia de interface TypeScript a partir de JSON e type guards

Resumo da ópera: gere a interface para economizar digitação, depois recheque opcionalidade e null, mantenha datas e inteiros grandes como strings e adicione validação em tempo de execução na fronteira; tratar os tipos gerados como contrato não lhe dá validação nenhuma.

Um gerador só vê a única amostra que você cola nele. Se os campos podem faltar, se os valores podem ser null e em que formato as datas vêm são todos palpites — e os palpites errados ficam para você corrigir.

1. O que o gerador realmente faz

Cole uma amostra JSON em JSON para TypeScript e a interface que retorna é um resumo de forma: ele conta as chaves e os tipos de valor que viu e então escolhe a representação mais estreita disponível.

{
  "id": 1,
  "email": "ada@example.com",
  "profile": { "nickname": "Ada", "age": 36 },
  "tags": ["admin", "beta"],
  "createdAt": "2026-10-31T08:00:00Z"
}

A saída fica mais ou menos assim:

interface User {
  id: number;
  email: string;
  profile: { nickname: string; age: number };
  tags: string[];
  createdAt: string;
}

Isso compila, mas descreve um valor ideal, não o contrato real do endpoint. O gerador não sabe se email pode faltar, se tags pode ser vazio ou se createdAt sempre traz fuso horário. Ele nem sabe que id pode ser string em outros registros.

Então a expectativa correta é: o gerador economiza digitação, o humano fornece o significado.

2. Campos opcionais e null: ? não é | null

Esta é a parte da saída gerada que é copiada com mais frequência, e a que quebra com mais frequência.

Forma TypeScript Significado JSON correspondente
email: string Sempre presente, sempre string {"email":"a@b.c"}
email?: string A chave pode estar ausente; quando presente, não pode ser null {} ou {"email":"a@b.c"}
email: string | null A chave está sempre presente, o valor pode ser null {"email":null}
email?: string | null A chave pode estar ausente e o valor pode ser null Os três casos

O gerador viu uma amostra, então normalmente emite a primeira forma. Se a API pode realmente retornar null, o tipo está mentindo: tudo compila e depois email.toLowerCase() lança erro em tempo de execução.

Há duas formas de corrigir:

  1. Colete várias amostras reais que cubram tanto "chave ausente" quanto "valor é null" e deixe o gerador inferir de novo.
  2. Após gerar, mude os campos à mão para ?: T | null e realmente trate os dois ramos no código.

Não tome o atalho de marcar tudo como ?: string. Opcional significa "a chave pode não existir", não "o valor pode ser vazio". undefined e null também se comportam de forma diferente em JSON.stringify: o primeiro é omitido, o segundo é mantido como null.

3. Elementos de array desiguais e tipos union

Arrays são a estrutura mais fácil de errar. Se tags aparece como ["admin"], [] e ["beta", 3] em três registros, o gerador pode emitir string[], (string | number)[] ou unknown[], dependendo das amostras que viu.

Um humano precisa decidir:

  • Os tipos dos elementos realmente são misturados: escreva (string | number)[] e estreite no ponto de uso.
  • Só um registro está sujo: corrija os dados. Não os esconda com any[].
  • Arrays vazios: o gerador não consegue inferir um tipo de elemento e recai em never[] ou any[], então você precisa preenchê-lo.
  • Tuplas de comprimento fixo: use [string, string] apenas quando o negócio realmente garante o comprimento; caso contrário, adicionar um elemento depois vira erro de compilação.

Arrays de objetos também precisam de atenção quando as chaves são desiguais:

interface OrderItem {
  sku: string;
  quantity: number;
  discount?: number; // alguns registros não trazem este campo
}

Coloque a opcionalidade no tipo do elemento, não no array. items?: OrderItem[] significa que o array em si pode estar ausente, o que é uma afirmação completamente diferente.

4. Datas, inteiros grandes, enums: tipos que o JSON não tem

O JSON tem apenas strings e números; o significado de negócio vive nas convenções. O gerador infere a partir de literais, então corrija à mão:

Literal JSON Saída gerada usual O que deveria ser
"2026-10-31T08:00:00Z" string Mantenha string na camada de transporte e converta no ponto de uso; nunca declare Date diretamente
1717075200000 number Use number para um timestamp em milissegundos e documente a unidade
"1234567890123456789" string Mantenha como string para que number não perca precisão
1 e "1" misturados number | string Normalize para uma forma; não empurre a union para o código de negócio
"admin" string Uma union literal 'admin' | 'editor', ou uma constante de enum compartilhada
true e "true" misturados boolean | string Corrija a fonte de dados em vez de gerar esse tipo

A regra mais importante: datas em JSON são sempre strings. Escrever createdAt: Date é um autoengano comum — JSON.parse nunca constrói um Date, então o valor em tempo de execução ainda é uma string e .getTime() lança erro. Mantenha o tipo de transporte como string, chame new Date(value) explicitamente na fronteira e verifique Number.isNaN.

Inteiros grandes funcionam do mesmo jeito. Assim que um id passa de 2^53, o JSON.parse já perdeu precisão, e gerar number só cimenta o bug no tipo. O backend retorna uma string, o frontend a mantém como string; essa é a única opção confiável.

5. Tipos gerados não fazem validação em tempo de execução

Tipos TypeScript são apagados em tempo de compilação. Não importa quão precisa seja a interface, ela não consegue impedir uma resposta {"id": "x"} — ela restringe o seu código, não os bytes que chegam pela rede.

/**
 * Verifica se um valor em tempo de execução é um objeto de usuário.
 * @param value O valor em tempo de execução a validar
 * @returns Se ele satisfaz a forma mínima de User
 */
function isUser(value: unknown): value is User {
  if (typeof value !== 'object' || value === null) {
    return false;
  }
  const candidate = value as Record<string, unknown>;
  return typeof candidate.id === 'number' && typeof candidate.email === 'string';
}

Um type guard só cobre os campos que você escreve na verificação. Conforme os campos se multiplicam, guards escritos à mão viram uma segunda definição para manter, afastando-se da interface.

O trade-off prático fica assim:

Abordagem Validação em tempo de execução Origem dos tipos Serve para
Apenas interface Nenhuma Escrita à mão ou gerada Dados internos confiáveis, restrições em tempo de compilação
Interface mais guard escrito à mão Sim Duas definições que divergem Poucos campos, fronteira clara
zod / valibot Sim Tipos inferidos do schema APIs externas, formulários, configuração
JSON Schema mais validador Sim Tipos gerados do schema Contratos entre linguagens, documentação

Se os dados vêm de fora, faça do schema a única fonte de verdade e derive os tipos com z.infer ou json-schema-to-ts. Assim "a validação passa" e "o tipo está correto" são o mesmo fato, e não há uma segunda definição para divergir. Para a abordagem completa de sincronização bidirecional, veja Sincronização entre Schema e tipos TypeScript.

6. Interface ou type: não discuta sobre a saída gerada

Geradores normalmente emitem interface, mas os dois são intercambiáveis na maioria das situações. O que realmente importa são três capacidades:

  • Declaration merging: interface pode ser declarada várias vezes e se mescla automaticamente; type não pode. Isso ajuda ao remendar tipos de terceiros, mas modelos de negócio devem evitá-lo — a mesclagem deixa "campos extras" passarem pelo compilador sem serem notados.
  • Unions e tipos mapeados: type pode expressar A | B, Partial<T> e tipos de template literal diretamente; interface não pode.
  • Recursão: ambos suportam, mas formas recursivas complexas se leem mais naturalmente como type.

Uma regra de equipe mais útil: use interface para formas de objeto, type para unions, tipos utilitários e tipos de função. Deixe a saída gerada no padrão e gaste o tempo de revisão em opcionalidade e validação em tempo de execução.

7. Cadeia de ferramentas: três formas de gerar

Abordagem Entrada Ponto forte Limitação
JSON para TypeScript do site JSON de amostra Funciona imediatamente, processado localmente Baseado apenas na amostra atual; precisa de estreitamento manual
quicktype JSON / JSON Schema Saída em várias linguagens, integração com CLI Inferência conservadora, unions podem ficar largas demais
json-schema-to-typescript JSON Schema Guiado por contrato e reproduzível Exige primeiro um schema confiável

Uma invocação típica do quicktype:

npx quicktype -s json -o user.ts user.json --just-types

--just-types emite apenas tipos e nenhum código de conversão em tempo de execução, o que convém a projetos que já têm uma camada de análise. Se o schema é escrito à mão ou produzido pelo backend, json-schema-to-typescript encaixa melhor porque respeita required, enum e additionalProperties — restrições que a inferência por amostra simplesmente não enxerga.

A geração de schema tem suas próprias armadilhas, como marcar todos os campos como obrigatórios ou transformar null em opcional silenciosamente; veja armadilhas comuns de geradores de JSON Schema.

8. Uma lista de verificação antes de commitar tipos gerados

Passe por estes cinco itens antes de commitar a saída:

  1. Normalize nomes de campos: não deixe userId e user_id coexistirem; mapeie uma vez em uma camada de conversão.
  2. Recheque a opcionalidade: compare com respostas reais e descubra quais campos podem faltar ou ser null.
  3. Mantenha datas e ids como strings: documente a unidade e o formato no tipo e analise explicitamente no ponto de uso.
  4. Reduza any e unknown: onde o gerador emitiu any, adicione um tipo ou mantenha unknown e adicione um guard.
  5. Adicione validação em tempo de execução: valide dados externos pelo menos uma vez; não espere que uma interface pegue dados ruins.

Se o projeto usa várias bibliotecas JSON (JSON.parse, superjson, lossless-json), o comportamento delas para inteiros grandes e datas difere, então confirme isso na seleção. Veja Seleção de bibliotecas de análise JSON.

Por que o gerador marca todo campo opcional como obrigatório?

Ele viu uma única amostra, então toda chave que apareceu é tratada como sempre presente. Forneça várias amostras que cubram casos diferentes, ou gere a partir de JSON Schema — o array required ali é a fonte autoritativa.

Por que declarar Date em uma interface falha em tempo de execução?

JSON.parse produz apenas strings, números, booleanos, null, objetos e arrays; ele nunca constrói um Date. Declarar Date é uma promessa de tempo de compilação enquanto o valor em tempo de execução continua sendo uma string. Mantenha string na camada de transporte e converta na fronteira.

Tipos gerados podem ser usados como validação?

Não. Tipos TypeScript são apagados após a compilação e não impõem nada aos dados em tempo de execução. Valide dados externos com zod, valibot, ajv ou um type guard escrito à mão.

Um id inteiro grande deve ser tipado como number ou string?

Se ele pode passar de 2^53, precisa ser string. JSON.parse perde precisão durante a análise, e tipar como number só escreve o bug no tipo, onde ele não pode mais ser corrigido.