JSON.parse devuelve any, así que TypeScript acepta cualquier tipo al que asignes el resultado. Eso hace que tipar JSON parseado cueste una línea, y también significa que el tipo es una promesa que haces tú, no algo que compruebe el compilador:
El segundo objeto tiene age como el string "41". TypeScript sigue llamándolo number, porque any se puede asignar a cualquier cosa, y el programa imprime 411. Para JSON que acaba de escribir tu propio código, la anotación está bien. Para datos que vienen de una petición, un archivo o el local storage, valídalos.
Parsear a unknown
Tipar el resultado como unknown hace que el compilador exija una comprobación antes de usar cualquier propiedad:
El error es index.ts(4,13): error TS18046: 'data' is of type 'unknown'. Cada comprobación que escribas después estrecha data un poco más.
Validar con un type guard
Un type guard es una función que devuelve value is User. Cuando devuelve true, TypeScript trata el valor como un User a partir de ahí, y las comprobaciones de dentro son comprobaciones reales en tiempo de ejecución:
JSON.parse lanza por sí mismo un SyntaxError con texto mal formado, así que el código real también lo envuelve en try/catch. Con datos grandes o anidados, los guards escritos a mano se alargan; librerías de esquemas como Zod o Valibot te permiten declarar la forma una sola vez y derivar de ella tanto el validador como el tipo de TypeScript.
De JSON a interfaz de TypeScript
Convertir una muestra de JSON en tipos es mecánico. Dada esta respuesta:
{
"id": 42,
"title": "Learn TypeScript",
"done": false,
"owner": { "id": 7, "name": "Ada" },
"tags": ["study", "ts"],
"dueDate": "2024-03-15T10:30:00Z",
"notes": null
}
Asigna a cada valor su tipo, da a los objetos anidados su propia interfaz y marca lo que puede variar:
Una sola muestra no puede decirte qué campos son opcionales o pueden ser null. Mira varias respuestas, o la documentación de la API, antes de decidir dónde van ? y | null.
Fechas y el reviver
JSON no tiene tipo fecha, así que las fechas llegan como strings. El segundo argumento de JSON.parse, el reviver, se llama para cada clave y puede reconstruirlas:
El parámetro value del reviver es any, y el resultado también, así que el tipo Order sigue siendo un acto de confianza y no una comprobación. JSON.stringify(order, null, 2) sangra la salida con dos espacios y convierte de nuevo el Date en su string ISO.
JSON.stringify y lo que pierde
JSON.stringify está tipado para devolver string. Los valores que convierte no siempre vuelven igual, y el tipo no te avisa:
| Valor | Después de JSON.stringify |
|---|---|
Date | string ISO (a través de su método toJSON) |
Map, Set | {} (conviértelos antes con [...set] u Object.fromEntries(map)) |
undefined, funciones, symbols dentro de un objeto | la clave se omite |
undefined, funciones, symbols dentro de un array | null |
undefined, una función o un symbol sueltos | undefined, no un string |
NaN, Infinity | null |
bigint | lanza un TypeError |
Las reglas en tiempo de ejecución son las mismas que en JavaScript normal, y se explican en JSON en JavaScript.
Un tipo para cualquier valor JSON
Cuando el código maneja JSON arbitrario, un tipo recursivo describe exactamente lo que puede contener JSON y rechaza lo que no:
Sin el comentario, la última línea es un error de compilación, porque un objeto Date no es un JsonValue.
Importar archivos .json
Un archivo .json se puede importar como un módulo, y TypeScript infiere su tipo a partir del contenido:
{ "name": "app", "port": 8080, "tags": ["a"] }
// CommonJS output, or a bundler
import config from "./config.json";
const port: number = config.port; // typed from the file: number
// An ES module under module: nodenext
import settings from "./config.json" with { type: "json" };
En TypeScript 7 esto funciona sin configuración extra con module en nodenext, node20, commonjs, esnext o preserve. Con node16 y node18 falla con el error TS2732, Cannot find module './config.json'. Consider using '--resolveJsonModule' to import module with '.json' extension., hasta que añades "resolveJsonModule": true; ponerlo en false desactiva los imports de JSON en todos los casos. En un ES module con nodenext o node20, el import necesita el atributo with { type: "json" } (error TS1543 sin él) y solo se permite el import por defecto (error TS1544 para import { port }). tsc copia el archivo .json importado a outDir junto al JavaScript compilado.
Preguntas frecuentes
¿Qué tipo devuelve JSON.parse en TypeScript?
any. El compilador no puede saber qué contiene un string, así que const user: User = JSON.parse(text) compila contenga lo que contenga el texto. Asigna el resultado a unknown y valídalo cuando los datos vengan de fuera de tu programa.
¿Cómo convierto JSON en una interfaz de TypeScript?
Toma una muestra representativa y escribe una propiedad por cada clave: los strings, números y booleanos corresponden a string, number y boolean, un objeto anidado pasa a ser su propia interfaz, un array de objetos pasa a ser Item[], y las claves que a veces faltan llevan ?. Generadores de código como quicktype lo automatizan, pero comprueba lo que deducen con más de una muestra.
¿Cómo importo un archivo JSON en TypeScript?
import config from "./config.json"; funciona en TypeScript 7 con module en nodenext, node20, commonjs, esnext o preserve, y el resultado se tipa a partir del contenido del archivo. Con node16 o node18, pon además "resolveJsonModule": true. En un ES module con nodenext o node20, añade el atributo que exige Node: import config from "./config.json" with { type: "json" };.
¿JSON.stringify siempre devuelve un string?
Su tipo dice string, pero JSON.stringify(undefined) y JSON.stringify(() => 1) devuelven undefined en tiempo de ejecución. Los valores dentro de objetos también se convierten: un Date pasa a ser un string ISO, y Map y Set pasan a ser {}.