JSON para tipos TypeScript
Cole a resposta de uma API ou um arquivo .json e receba as interfaces (ou os type aliases) do TypeScript e o JSON Schema 2020-12, com campos opcionais deduzidos de todos os elementos de uma lista, e não só do primeiro. O JSON não sai do seu navegador.
Por que olhar só o primeiro elemento não basta
O JSON que vem de uma API quase nunca tem todos os objetos iguais. Em uma lista de pedidos, o primeiro tem notas e o segundo não; o terceiro tem o código de rastreio porque já foi enviado. Um gerador que olha só o primeiro elemento escreve um tipo com notas obrigatórias e sem rastreio: o código compila, e no meio da execução você encontra undefined onde o tipo prometia uma string.
Aqui cada elemento de cada lista é lido, inclusive listas dentro de listas. Basta uma chave faltar em um só objeto para ela virar opcional (rastreio?: string). Um campo que ora é número, ora é string vira number | string, e a página sinaliza o caso indicando quantas vezes aparece cada tipo, porque muitas vezes são dados sujos: um preço escrito uma vez entre aspas.
Estar ausente e ser null não é a mesma coisa, e o tipo mostra isso. Uma chave que às vezes falta recebe o ponto de interrogação; uma chave que está sempre lá mas às vezes é null recebe | null; se acontecerem as duas coisas, recebe as duas, e com --strict o TypeScript trata os dois casos de forma diferente.
As escolhas que você vai encontrar no resultado
Uma lista que está sempre vazia vira unknown[] e não any[]. Nada se sabe sobre os elementos dela, e unknown obriga você a conferi-los antes de usar, enquanto any desligaria a verificação justamente onde ela é necessária. Um objeto que está sempre vazio vira Record<string, unknown> pelo mesmo motivo.
Chaves que não são nomes válidos ficam entre aspas: "data-nascimento", "first name", "2fa". Palavras reservadas como default ou class, por outro lado, são permitidas como nomes de propriedade e ficam sem aspas.
Os nomes das interfaces aninhadas vêm da chave: endereco_entrega vira EnderecoEntrega. Para os elementos de uma lista, o nome é passado para o singular, com uma heurística declarada que olha como a palavra termina em italiano, inglês e espanhol (utenti dá Utente, categories dá Category, direcciones dá Direccion). Terminações que podem significar duas coisas são deixadas como estão: o italiano -umi vem tanto de volume quanto de consumo, então legumi continua Legumi, enquanto as palavras mais comuns estão listadas uma a uma (consumi dá Consumo). O que a terminação não resolve é a mesma palavra em dois idiomas: roles é role em inglês e rol em espanhol, e vence o inglês. Por isso cada nome passado para o singular aparece listado abaixo do resultado.
Dois objetos com a mesma forma viram um único tipo: se os endereços de entrega e de cobrança têm as mesmas chaves, sai Endereco, usado duas vezes. Se os nomes não têm nada em comum, como author e category que são ambos um id e um nome, o segundo fica como alias, type Category = Author, porque category: Author passaria a ideia errada. E um nome que coincide com um tipo que o TypeScript ou o navegador já tem (Date, Event, Location, AudioData) recebe na frente o nome do pai, para não esconder o tipo verdadeiro no arquivo inteiro.
interface ou type: qual escolher
Para descrever dados, as duas formas são equivalentes: escolha a que seu projeto já usa. As interfaces podem ser estendidas e são o que a documentação do TypeScript recomenda para objetos; os type aliases são necessários quando o tipo não é um objeto, então uma raiz que é uma lista ou um valor simples sai como type alias mesmo que você tenha escolhido interface, por exemplo export type Raiz = RaizElemento[];, e a página avisa. Tudo sai com export.
JSON Schema, para conferir os dados quando chegam
Os tipos do TypeScript desaparecem quando o código roda: protegem quem escreve o programa, não o programa dos dados que recebe. Para conferir uma resposta quando ela chega é preciso um schema, e a segunda saída é um JSON Schema draft 2020-12, lido pelo Ajv em JavaScript, pelo jsonschema em Python e pela maioria das ferramentas que validam configurações e APIs.
O schema espelha os tipos: as mesmas interfaces ficam em $defs, os campos obrigatórios em required, nulos e campos mistos em um type com vários valores. Ele distingue integer de number pela forma como o número é escrito: 10 é um inteiro, 10.0 não é, porque quem escreve o ponto está dizendo que ali podem chegar decimais.
A opção schema fechado acrescenta additionalProperties: false: uma chave que não estava na amostra vira erro. Ative-a para dados que você mesmo produz; deixe-a desligada para a API de terceiros, que amanhã pode acrescentar um campo e fazer com que respostas válidas sejam rejeitadas.
Números grandes demais e chaves repetidas
Em JavaScript um número inteiro é exato só até 9.007.199.254.740.991, ou seja, 2 elevado a 53 menos 1. Acima disso, os últimos dígitos mudam sozinhos: 9007199254740993 lido com JSON.parse vira 9007199254740992, sem avisar. Acontece com identificadores, e por isso algumas APIs mandam o id também como string. Esta página lê o JSON com um leitor que mantém os dígitos originais e diz onde está um número assim e no que ele se transforma. No tipo continua number; o conselho é pedir que seja enviado como string.
O mesmo vale para chaves repetidas no mesmo objeto: o JSON.parse guarda a última sem avisar, e aqui elas são sinalizadas com a linha. E um JSON quebrado recebe linha, coluna e o motivo em palavras: a vírgula sobrando antes de fechar um objeto, aspas simples, um comentário, um True escrito à moda do Python.
Como foi testado
Um gerador de tipos pode errar de um jeito que não se vê: tipos todos opcionais, ou todos any, sempre compilam. Por isso o teste pega centenas de arquivos JSON gerados ao acaso (alguns com a mesma forma repetida em vários pontos, cada um com sua variante) e faz o compilador do TypeScript, com --strictcompilar os tipos produzidos junto com o próprio JSON. Depois ele remove um campo obrigatório, muda o tipo de um valor ou acrescenta uma chave inventada, e exige que o compilador rejeite o arquivo.
O schema é conferido por uma biblioteca que não é nossa, jsonschema para Python: o JSON de partida precisa ser válido, a versão quebrada não. O leitor de JSON é comparado com o do Python em milhares de textos estragados de propósito e dá as mesmas respostas, com duas diferenças deliberadas: tolera o BOM invisível que alguns programas do Windows colocam no começo de um arquivo, e rejeita NaN e Infinity, que não existem em JSON.
O que ele não faz
Não adivinha significados. Uma data continua string, e um e-mail também: pelo texto não dá para saber se amanhã chegará um formato diferente. Do mesmo modo, o status de um pedido, com três valores vistos, continua string e não vira a lista desses três: os valores permitidos quem escreve é você.
Não reconhece dicionários. Um objeto que usa identificadores como chaves vira uma interface com essas chaves; se na verdade é um mapa, troque por Record<string, Usuario>. E não inventa tipos recursivos: uma árvore vira interfaces aninhadas, tão profundas quanto a amostra.
Não escreve código de validação nem classes, e não conserta JSON quebrado: para isso existe o Reparar JSON, e para reformatá-lo e ver onde ele quebra, use Formatar e validar JSON. Para tirar uma tabela de um JSON existe o JSON para CSV, e se o arquivo for YAML, como um docker-compose ou um manifesto do Kubernetes, passe antes pelo YAML para JSON.