De JSON a tipos TypeScript

Pega la respuesta de una API o un archivo .json: salen las interfaces (o los type aliases) de TypeScript y el JSON Schema 2020-12, con los campos opcionales sacados de todos los elementos de una lista, no solo del primero. El JSON no sale de tu navegador.

O toca aquí o arrastra un archivo .json
El JSON se lee y se convierte en tu navegador: no se envía a ningún sitio, así que puedes pegar también una respuesta con datos reales.

Por qué no basta con mirar el primer elemento

El JSON que llega de una API casi nunca tiene todos los objetos iguales. En una lista de pedidos el primero tiene notas y el segundo no; el tercero tiene el seguimiento porque ya ha salido. Un generador que solo mira el primer elemento escribe un tipo con las notas obligatorias y sin el seguimiento: el código compila, y luego a mitad de ejecución encuentras undefined donde el tipo prometía un texto.

Aquí se leen todos los elementos de cada lista, también de las listas dentro de listas. Una clave que falta aunque sea en un solo objeto se vuelve opcional (tracking?: string). Un campo que vale ahora un número, ahora un texto se convierte en number | string, y la página lo señala con cuántas veces aparece cada tipo, porque a menudo es un dato sucio: un precio escrito una vez entre comillas.

Faltar y valer null no son lo mismo, y el tipo lo dice. Una clave que a veces no está lleva el signo de interrogación; una clave que siempre está pero a veces vale null lleva | null; si pasan las dos cosas, lleva las dos, y con --strict TypeScript trata los dos casos de forma distinta.

Las decisiones que encuentras en el resultado

Una lista siempre vacía se convierte en unknown[] y no en any[]. De sus elementos no se sabe nada, y unknown te obliga a comprobarlos antes de usarlos, mientras que any apagaría la comprobación justo donde hace falta. Un objeto siempre vacío se convierte en Record<string, unknown> por el mismo motivo.

Las claves que no son nombres válidos se quedan entre comillas: "fecha-nacimiento", "first name", "2fa". Las palabras reservadas como default o class, en cambio, están admitidas como nombres de propiedad y se quedan sin comillas.

Los nombres de las interfaces anidadas salen de la clave: direccion_envio se convierte en DireccionEnvio. Para los elementos de una lista el nombre va en singular, una heurística declarada que mira cómo acaba la palabra en italiano, en inglés y en español (utenti da Utente, categories da Category, direcciones da Direccion). Las terminaciones que pueden significar dos cosas las deja en paz: del italiano -umi salen tanto volume como consumo, así que legumi se queda en Legumi, mientras que las palabras más comunes están escritas una por una (consumi da Consumo). Lo que la terminación no puede resolver es la misma palabra en dos idiomas: roles es role en inglés y rol en español, y gana el inglés. Por eso cada nombre puesto en singular aparece bajo el resultado.

Dos objetos con la misma forma se convierten en un solo tipo: si la dirección de envío y la de facturación tienen las mismas claves sale Direccion, usada dos veces. Si los nombres no tienen nada en común, como author y category que son los dos un id y un nombre, el segundo se queda como alias, type Category = Author, porque category: Author se leería mal. Y un nombre que coincide con un tipo que TypeScript o el navegador ya tienen (Date, Event, Location, AudioData) lleva delante el nombre del padre, para no tapar el verdadero en todo el archivo.

interface o type: cuál elegir

Para describir datos las dos formas son equivalentes: elige la que ya usa tu proyecto. Las interfaces se pueden extender y son lo que la documentación de TypeScript aconseja para los objetos; los type aliases hacen falta cuando el tipo no es un objeto, así que una raíz que es una lista o un valor simple sale como type aunque hayas elegido interface, por ejemplo export type Raiz = RaizElemento[];, y la página lo dice. Todo sale con export.

JSON Schema, para comprobar los datos cuando llegan

Los tipos de TypeScript desaparecen cuando el código se ejecuta: protegen a quien escribe el programa, no al programa de los datos que recibe. Para comprobar una respuesta cuando llega hace falta un esquema, y la segunda salida es un JSON Schema draft 2020-12, que leen Ajv en JavaScript, jsonschema en Python y casi todas las herramientas que validan configuraciones y API.

El esquema refleja los tipos: las mismas interfaces están en $defs, los campos obligatorios en required, los null y los campos mixtos en un type con varios valores. Distingue integer de number por cómo está escrito el número: 10 es un entero, 10.0 no, porque quien escribe el punto está diciendo que ahí pueden llegar decimales.

La casilla del esquema cerrado añade additionalProperties: false: una clave que no estaba en el ejemplo se convierte en un error. Actívala para los datos que produces tú; déjala apagada para la API de otro, que mañana puede añadir un campo y hacerte rechazar respuestas buenas.

Números demasiado grandes y claves repetidas

En JavaScript un número entero es exacto solo hasta 9.007.199.254.740.991, es decir 2 elevado a 53 menos 1. Más allá, las últimas cifras cambian solas: 9007199254740993 leído con JSON.parse se convierte en 9007199254740992, sin decir nada. Pasa con los identificadores, y por eso algunas API mandan el id también como texto. Esta página lee el JSON con un lector que conserva las cifras originales, y te dice dónde está un número así y en qué se convierte. En el tipo se queda number; el consejo es que llegue como string.

Lo mismo con las claves repetidas en el mismo objeto: JSON.parse se queda con la última sin avisar, y aquí se señalan con la línea. Y un JSON roto recibe línea, columna y el motivo en palabras: la coma de más antes de una llave, las comillas simples, un comentario, un True escrito a la manera de Python.

Cómo se ha probado

Un generador de tipos puede equivocarse de una forma que no se ve: tipos todos opcionales, o todos any, compilan siempre. Por eso la prueba toma cientos de JSON generados al azar (algunos con la misma forma repetida en varios puntos, cada uno con su variante) y hace compilar al compilador de TypeScript, con --strict, los tipos producidos junto con el propio JSON. Luego quita un campo obligatorio, cambia el tipo de un valor o añade una clave inventada, y exige que el compilador rechace el archivo.

El esquema lo comprueba una biblioteca que no es nuestra, jsonschema de Python: el JSON de partida tiene que ser válido, la versión rota no. El lector del JSON se compara con el de Python sobre miles de textos estropeados a propósito y da las mismas respuestas, con dos diferencias a propósito: tolera el BOM invisible que algunos programas de Windows ponen al principio del archivo, y rechaza NaN e Infinity, que en JSON no existen.

Lo que no hace

No adivina los significados. Una fecha se queda en string, un email también: por el texto no se puede saber si mañana llegará un formato distinto. Así el estado de un pedido, con tres valores vistos, se queda en string y no se convierte en la lista de esos tres: los valores admitidos los escribes tú.

No reconoce los diccionarios. Un objeto que usa los identificadores como claves se convierte en una interfaz con esas claves; si en realidad es un mapa, cámbialo por Record<string, Usuario>. Y no inventa tipos recursivos: un árbol se convierte en interfaces anidadas tan profundas como el ejemplo.

No escribe código de validación ni clases, y no repara un JSON roto: para eso está Reparar JSON, y para volver a maquetarlo y ver dónde se rompe está Formatear y validar JSON. Si de un JSON necesitas una tabla está JSON a CSV, y si el archivo es YAML, como un docker-compose o un manifiesto de Kubernetes, pasa antes por De YAML a JSON.