CodeForge

TypeScript de Cero a Experto / TS en el mundo real

Tipar fetch: la gran mentira de response.json()

Teoría20 min20 XP

fetch es la puerta a los datos del mundo, y TypeScript la tipa… hasta cierto punto. Aquí descubres su gran mentira: response.json() devuelve any —el agujero por donde se cuela todo lo que TS intenta evitar—. Aprenderás a tipar fetch con genéricos, a ver por qué ese tipado es una PROMESA no verificada, y por qué eso te lleva directo a la validación en runtime.

El mensajero que no revisa el paquete

Tipar fetch con un wrapper genérico

El patrón habitual: una función genérica que hace fetch, chequea response.ok (¡la trampa del módulo 10 de JS!) y devuelve los datos "tipados":

fetch-generico.ts
// un helper genérico: T es el tipo que ESPERAS recibir
async function cargarJSON<T>(url: string): Promise<T> {
const respuesta = await fetch(url);
if (!respuesta.ok) {
  throw new Error("HTTP " + respuesta.status);
}
return respuesta.json();   // ⚠️ json() devuelve Promise<any> → se "convierte" a T
}

type Usuario = { id: number; nombre: string };

// lo usas indicando el tipo esperado:
const usuario = await cargarJSON<Usuario>("/api/usuario/1");
usuario.nombre.toUpperCase();   // TS te deja... porque CREE que es Usuario

cargarJSON<Usuario>(...) devuelve Promise<Usuario>, y a partir de ahí TS te trata el resultado como un Usuario con todo el autocompletado. Cómodo. Pero mira la línea del return respuesta.json(): ahí está el truco.

La mentira: json() devuelve any

response.json() está tipado como Promise<any>. Ese any (módulo 2) es el problema: acepta convertirse en CUALQUIER tipo sin verificar nada:

la-mentira.ts
// esto compila SIN error, aunque sea una mentira total:
const usuario = await cargarJSON<Usuario>("/api/cualquier-cosa");

// si el servidor devolvió { error: "no encontrado" } en vez de un Usuario:
usuario.nombre.toUpperCase();
// ✅ para TS (cree que es Usuario)
// 💥 en RUNTIME: usuario.nombre es undefined → Cannot read properties of undefined

// el 'any' de json() significa: TS confía en tu <Usuario> a ciegas.
// tú PROMETISTE que era un Usuario; TS no lo comprobó, y no puede.

Lo correcto: unknown y validación

La respuesta honesta es tipar el dato crudo como unknown (módulo 2) y VALIDARLO antes de confiar —con un type guard (módulo 4) o, mejor, con una librería:

lo-correcto.ts
// honesto: lo que llega es 'unknown' hasta que se valide
async function cargarSeguro(url: string): Promise<unknown> {
const respuesta = await fetch(url);
if (!respuesta.ok) throw new Error("HTTP " + respuesta.status);
return respuesta.json();   // unknown: "no sé qué es, valídalo"
}

// un type guard (módulo 4) verifica la forma en runtime:
function esUsuario(x: unknown): x is Usuario {
return typeof x === "object" && x !== null &&
  "id" in x && "nombre" in x;
}

const dato = await cargarSeguro("/api/usuario/1");
if (esUsuario(dato)) {
dato.nombre.toUpperCase();   // ✓ AHORA sí es seguro, verificado en runtime
} else {
throw new Error("El servidor no devolvió un Usuario válido");
}

Escribes const u = await cargarJSON<Usuario>(url) y usas u.nombre sin errores de TS. Pero en producción a veces sale 'Cannot read properties of undefined'. ¿Por qué?

Mini-reto

Cierra el agujero: 1) escribe type Producto = { id: number; precio: number } y un type guard esProducto(x: unknown): x is Producto; 2) simula un "servidor" con una función que devuelve Promise<unknown> (a veces un producto válido, a veces { malo: true }); 3) valida con el guard antes de usar el precio, y comprueba que los datos inválidos NO pasan. Sientes dónde terminan los tipos y dónde empieza la validación.

Qué sigue

Escribir type guards a mano para cada tipo es tedioso y propenso a errores. La próxima lección trae la herramienta que lo automatiza: Zod —defines un esquema UNA vez y obtienes la validación en runtime Y el tipo de TypeScript, sincronizados. El puente definitivo entre el mundo exterior y tus tipos.