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.

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:
- Colete várias amostras reais que cubram tanto "chave ausente" quanto "valor é null" e deixe o gerador inferir de novo.
- Após gerar, mude os campos à mão para
?: T | nulle 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[]ouany[], 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:
interfacepode ser declarada várias vezes e se mescla automaticamente;typenã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:
typepode expressarA | B,Partial<T>e tipos de template literal diretamente;interfacenã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:
- Normalize nomes de campos: não deixe
userIdeuser_idcoexistirem; mapeie uma vez em uma camada de conversão. - Recheque a opcionalidade: compare com respostas reais e descubra quais campos podem faltar ou ser
null. - Mantenha datas e ids como strings: documente a unidade e o formato no tipo e analise explicitamente no ponto de uso.
- Reduza
anyeunknown: onde o gerador emitiuany, adicione um tipo ou mantenhaunknowne adicione um guard. - 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.