JSON para TypeScript

Gera interfaces TypeScript a partir de JSON: elementos de array mesclados, chaves opcionais, uniões de tipos mistos, null distinto — tudo no navegador.

Entrada
TypeScript
export interface Root {
  id: number
  name: string
  active: boolean
  address: Address
  roles: string[]
  posts: Post[]
  tags: unknown[]
}

export interface Address {
  city: string
  zip: null
}

export interface Post {
  id: number
  title: string
  views: number | string
  pinned?: boolean
}

Interfaces: 3

Tipos inferidos dos dados, não adivinhados

Um documento JSON não carrega os próprios tipos: carrega valores, e os tipos precisam ser lidos de volta a partir deles. Isso é fácil para um único objeto e surpreendentemente sutil para uma coleção: a forma que você quer não é a de nenhum registro específico, mas a forma que todo registro precisa satisfazer. Esta ferramenta infere interfaces TypeScript a partir de uma amostra JSON olhando tudo o que a amostra contém, de modo que os tipos que ela produz descrevem a totalidade dos seus dados e não a primeira linha que por acaso ficou no topo.

A conversão vai em um só sentido. Converter os tipos de volta para JSON exigiria inventar valores, e aqui o objetivo é o oposto: o documento colado é a fonte da verdade, e cada declaração deriva dele. Cole uma resposta de uma API, um arquivo de configuração ou uma linha de log, e leia a interface que você teria escrito à mão de outro modo.

Como os arrays são mesclados

As decisões interessantes acontecem todas nos arrays. Um conversor ingênuo olha o primeiro elemento e para, o que faz todo campo que ele vê por acaso parecer obrigatório e ignora todo campo que ele não vê. Esta ferramenta, em vez disso, mescla todos os elementos em um único tipo, e três coisas decorrem disso:

  • Uma chave presente em alguns elementos mas ausente em outros torna-se opcional, escrita com um ponto de interrogação. Se metade dos seus registros tem "middleName" e metade não, o campo é "middleName?", exatamente o que um consumidor precisa tratar.
  • Uma chave cujo valor difere em tipo entre os elementos torna-se uma união. Um campo que é um número em um registro e uma string em outro é tipado como "number | string" — não por capricho, mas porque é o que os dados realmente contêm e o que o seu código precisa aceitar.
  • Um objeto aninhado dentro dos elementos é mesclado do mesmo jeito, recursivamente, e extraído para a sua própria interface. Dez elementos de um array que carregam cada um um "address" produzem uma única interface Address descrevendo os dez.

Quando o próprio documento é um array no nível superior, a raiz vira um alias para uma interface de item — por exemplo "type Root = RootItem[]" — com o tipo do elemento mesclado escrito abaixo.

Null, opcional e por que são diferentes

É tentador tratar um null igual a uma chave ausente, e é um erro. Em TypeScript "name?: string" significa que a propriedade pode faltar; "name: string | null" significa que ela sempre está lá mas pode conter null. São contratos diferentes, e um consumidor os verifica de forma diferente — "in" contra uma comparação de valor. Esta ferramenta os mantém separados: um null explícito nos dados torna-se um membro de união "| null", colocado por último para que "string | null" se leia como você espera, enquanto uma chave que simplesmente falta em alguns registros torna-se opcional. Um campo que é as duas coisas — null em um registro, ausente em outro — sai como ambas, "field?: T | null", porque os dois fatos são verdadeiros sobre os seus dados.

Objetos aninhados viram interfaces nomeadas

Em vez de embutir uma forma aninhada dentro do pai, cada objeto é extraído para a sua própria interface cujo nome deriva da chave sob a qual ele está. Um objeto "user" vira uma interface User; um "address" dentro dele vira uma interface Address a que User faz referência. Tipos embutidos em profundidade são difíceis de ler e impossíveis de reutilizar, e interfaces nomeadas são o que você mesmo teria escrito. Elementos de um array são postos no singular quando possível — "users" dá um User, "categories" um Category — e uma chave que não pluraliza recebe o sufixo Item para que o elemento tenha um nome próprio.

Se dois objetos diferentes tomassem o mesmo nome — dois "data" sem relação, digamos — o segundo recebe um sufixo em vez de ser mesclado, de modo que formas distintas permaneçam distintas. O objeto raiz é emitido primeiro e você pode renomeá-lo; a escolha entre saída "interface" e "type" é um interruptor, já que algumas bases de código preferem aliases de tipo em toda parte.

O recuo para unknown

Alguns valores não carregam informação de tipo nenhuma. Um array vazio poderia conter qualquer coisa; um objeto vazio não tem chaves a descrever. Em vez de recorrer a "any" — que desliga a verificação de tipos para tudo o que segue — a ferramenta recua para "unknown": um array vazio vira "unknown[]", um objeto vazio vira "Record<string, unknown>". A diferença importa. "any" deixa passar bugs em silêncio; "unknown" obriga o consumidor a estreitar o valor antes de usá-lo, de modo que o tipo inferido continua honesto sobre o que a amostra disse e o que não disse.

São os tipos que você apertaria à mão assim que souber o que a coleção vazia deve conter — mas até os dados dizerem, "unknown" é a resposta honesta, e é a que mantém seguros os demais tipos.

Em que ele roda, e onde

Tudo acontece no seu navegador. O JSON é analisado e os tipos são inferidos no seu próprio dispositivo; nada do que você cola é enviado, armazenado ou registrado. Isso torna a ferramenta segura para usar em uma resposta de API real ou um arquivo de configuração com segredos dentro: a amostra nunca sai da página. A saída é TypeScript comum que você pode colar diretamente em um arquivo "d.ts" ou em um módulo, ajustar os poucos campos "unknown" que os dados não puderam descrever, e usar.

Perguntas frequentes

Meu JSON é enviado para um servidor?
Não. O documento é analisado e os tipos são inferidos inteiramente no seu navegador, e nada do que você cola é enviado ou registrado. É seguro usar em uma resposta de API real ou um arquivo de configuração.
Por que um campo é opcional se está presente na minha amostra?
Porque ele está ausente de pelo menos um elemento de um array que a ferramenta mesclou. A inferência lê todos os elementos, não apenas o primeiro, então uma chave que alguns registros omitem torna-se opcional — é o tipo que os seus dados realmente admitem, mesmo que o registro que você olhou por acaso a incluísse.
Por que um campo virou uma união como string | number?
Porque o valor tinha tipos diferentes em elementos diferentes do array — uma string em um registro e um número em outro. O tipo mesclado precisa aceitar ambos, então é escrito como uma união. Se isso surpreende você, geralmente significa que os dados são menos uniformes do que o esperado, o que vale saber.
Por que a ferramenta usa unknown em vez de any?
Para valores que ela não pode descrever — um array vazio, um objeto vazio — "unknown" mantém o resultado seguro em tipos, obrigando um consumidor a estreitar o valor antes de usá-lo, enquanto "any" desligaria a verificação de tipos. Você pode apertar esses campos à mão assim que souber o que a coleção vazia contém.
Qual é a diferença entre a saída interface e type?
Nenhuma nos tipos que descrevem — ambas produzem as mesmas formas. "interface" é o idioma comum para tipos de objeto e pode ser estendido e mesclado; aliases "type" são o que algumas bases de código preferem usar em toda parte. O interruptor existe para que a saída combine com o estilo do seu projeto.
Ele pode converter TypeScript de volta para JSON?
Não, e de propósito. A conversão é de mão única: JSON entra, tipos saem. Ir na outra direção exigiria inventar valores que nunca estiveram nos seus dados, e todo o objetivo é que cada declaração derive do que você realmente colou.
Como os objetos aninhados são nomeados?
Pela chave sob a qual estão: um objeto "user" vira User, um "address" dentro dele vira Address. Elementos de um array são postos no singular quando possível — "categories" dá Category — e uma chave que não pluraliza recebe o sufixo Item. Duas formas diferentes que colidiriam em um nome recebem um sufixo em vez de serem mescladas, então permanecem distintas.