JSON.parse devuelve any, y any apaga TypeScript para todo lo que toca: puedes escribir mal una propiedad y nadie avisa. Vemos tres formas de leer un archivo de configuración en JSON sin perder la seguridad de los tipos, y cuándo conviene cada una.
También te puede interesar
El archivo
{
"puerto": 8080,
"modo": "produccion",
"admins": ["ana@ejemplo.com", "luis@ejemplo.com"]
}El problema: any
import { readFile } from "node:fs/promises";
// JSON.parse devuelve any: TypeScript deja de revisar todo lo que salga de ahí.
const config = JSON.parse(await readFile("config.json", "utf8"));
console.log(config.puerto + 1);
console.log(config.puertoo); // errata: ningún aviso
console.log(config.admins.toUpperCase); // tampoco: es un array, no un textonpx tsc --noEmit --strict --target esnext --module nodenext --types node --locale es any.ts
tsc(sin errores)
node any.ts
salida8081 undefined undefined
Compila sin un aviso, pero hay dos errores: puertoo está mal escrito y admins es un array, que no tiene toUpperCase. Al ejecutar salen dos undefined silenciosos.
Forma 1: unknown y una guarda de tipo
Si guardas el resultado como unknown, TypeScript no deja usarlo hasta que lo compruebes:
interface Config {
puerto: number;
modo: "desarrollo" | "produccion";
admins: string[];
}
const datos: unknown = JSON.parse('{"puerto": 8080}');
console.log(datos.puerto);
const config: Config = datos;npx tsc --noEmit --strict --target esnext --module nodenext --types node --locale es guarda-error.ts
tscguarda-error.ts(7,13): error TS18046: "datos" es de tipo "unknown". guarda-error.ts(8,7): error TS2322: El tipo 'unknown' no se puede asignar al tipo 'Config'.
La comprobación se escribe en una guarda de tipo: una función que devuelve x is Config. Si devuelve true, TypeScript da por hecho que es un Config:
import { readFile } from "node:fs/promises";
interface Config {
puerto: number;
modo: "desarrollo" | "produccion";
admins: string[];
}
// Una guarda de tipo: si devuelve true, TypeScript da por hecho que x es Config.
function esConfig(x: unknown): x is Config {
if (typeof x !== "object" || x === null) return false;
const c = x as Record<string, unknown>;
return (
typeof c.puerto === "number" &&
(c.modo === "desarrollo" || c.modo === "produccion") &&
Array.isArray(c.admins) &&
c.admins.every((a) => typeof a === "string")
);
}
async function cargar(ruta: string): Promise<Config> {
const datos: unknown = JSON.parse(await readFile(ruta, "utf8"));
if (!esConfig(datos)) throw new Error(`${ruta} no tiene el formato esperado`);
return datos; // aquí ya es Config
}
const config = await cargar("config.json");
console.log(`Puerto ${config.puerto + 1}, modo ${config.modo}, ${config.admins.length} admins`);
try {
await cargar("malo.json");
} catch (e) {
console.log(e instanceof Error ? e.message : e);
}npx tsc --noEmit --strict --target esnext --module nodenext --types node --locale es guarda.ts
node guarda.ts
salidaPuerto 8081, modo produccion, 2 admins malo.json no tiene el formato esperado
El archivo malo.json trae el puerto como texto y un modo que no existe, y se rechaza al cargarlo:
{
"puerto": "8080",
"modo": "pruebas"
}esConfig compruebe de verdad todos los campos: si olvidas uno, te creerá igual. Para estructuras grandes, un esquema de Zod es más seguro, porque la comprobación y el tipo salen de la misma definición.Forma 2: importar el JSON
Si el archivo va con el código, se puede importar como un módulo. TypeScript lee el archivo y deduce su tipo. Hace falta --resolveJsonModule (o "resolveJsonModule": true en el tsconfig.json):
// Importar el JSON como módulo: TypeScript deduce el tipo leyendo el archivo.
import config from "./config.json" with { type: "json" };
console.log(config.puerto + 1, config.modo, config.admins[0]);npx tsc --noEmit --strict --target esnext --module nodenext --types node --locale es --resolveJsonModule importar.ts
node importar.ts
salida8081 produccion ana@ejemplo.com
Ahora las erratas sí se detectan. Pero el tipo deducido es el más general: modo es string, no "desarrollo" | "produccion":
import config from "./config.json" with { type: "json" };
console.log(config.puertoo);
const modo: "desarrollo" | "produccion" = config.modo;tscimportar-error.ts(2,20): error TS2551: La propiedad "puertoo" no existe en el tipo "{ puerto: number; modo: string; admins: string[]; }". ¿Quería decir "puerto"?
importar-error.ts(3,7): error TS2322: El tipo 'string' no se puede asignar al tipo '"desarrollo" | "produccion"'.Escribir JSON
Al guardar, el tipo protege lo que escribes: un objeto mal formado no compila. JSON.stringify con null, 2 lo deja con sangría:
import { readFile, writeFile } from "node:fs/promises";
interface Config {
puerto: number;
modo: "desarrollo" | "produccion";
admins: string[];
}
// Al guardar, el tipo protege lo que escribes: un objeto mal formado no compila.
const nueva: Config = { puerto: 3000, modo: "desarrollo", admins: ["ana@ejemplo.com"] };
await writeFile("/tmp/config-nueva.json", JSON.stringify(nueva, null, 2) + "\n", "utf8");
console.log(await readFile("/tmp/config-nueva.json", "utf8"));node guardar.ts
salida{
"puerto": 3000,
"modo": "desarrollo",
"admins": [
"ana@ejemplo.com"
]
}| Forma | Cuándo usarla |
|---|---|
unknown y guarda de tipo | JSON que llega de fuera y es pequeño. Sin dependencias. |
import … with { type: "json" } | Archivos que van con el código y que tú controlas. |
| Un esquema de Zod | Datos de fuera con estructura grande, o que cambian a menudo. |
Todo el código de esta página se comprobó con TypeScript 7.0.2 en modo estricto (--strict --locale es) y se ejecutó con Node.js 24.22 en un contenedor limpio antes de publicarla; los mensajes de error y las salidas están copiados de esa ejecución. Documentación oficial: Type predicates en el manual de TypeScript.