Saltar al contenido

Basta de usar any: cómo acotar unknown en TypeScript

Por qué unknown reemplaza a any de forma segura y qué patrones de acotamiento lo hacen práctico en código real.

5 min de lectura
Editor de código mostrando el acotamiento del tipo unknown en TypeScript con type guards

Todo código base que he heredado tiene la misma señal delatora: un grep de any devuelve cientos de resultados, y la mitad están justo en los puntos donde luego apareció un bug en producción. any no es un tipo — es una instrucción para que el compilador deje de verificar. unknown te da la misma flexibilidad en el límite de tu programa sin renunciar a la red de seguridad en todo lo que viene después. La razón por la que los equipos no lo usan más es que nadie les enseñó a acotarlo sin escribir un muro de if. Esa es la habilidad real.

Lo que realmente te cuesta any

any es contagioso. En cuanto un valor tiene tipo any, cada acceso a una propiedad, cada llamada a función, cada operación aritmética sobre él también es any — y TypeScript propaga eso felizmente por toda tu cadena de llamadas sin un solo error.

tstypescript
// ❌ any silently disables checking for everything downstream
function parseConfig(raw: any) {
  return {
    port: raw.port,
    // typo goes unnoticed — "confg.hsot" compiles fine
    host: raw.hsot,
    timeout: raw.timeout * 1000,
  };
}
 
const config = parseConfig(JSON.parse(fileContents));
config.host.toUpperCase(); // runtime crash: host is undefined

El compilador no dio ninguna advertencia aquí. raw.hsot es un error de tipeo, raw.timeout * 1000 asume un número que podría ser un string, y config.host es any hasta el punto de la llamada donde finalmente explota — en tiempo de ejecución, frente a un usuario, tres archivos lejos de donde realmente se cometió el error.

unknown como el límite type-safe

unknown es la contraparte type-safe que describe la sección del TypeScript Handbook sobre narrowing: puedes asignarle cualquier cosa, pero no puedes hacer nada con él hasta haber probado qué es.

tstypescript
// ✅ unknown forces you to prove the shape before using it
function parseConfig(raw: unknown): { port: number; host: string } {
  if (
    typeof raw !== "object" ||
    raw === null ||
    !("port" in raw) ||
    !("host" in raw)
  ) {
    throw new Error("Invalid config shape");
  }
 
  const { port, host } = raw as { port: unknown; host: unknown };
 
  if (typeof port !== "number" || typeof host !== "string") {
    throw new Error("Invalid config field types");
  }
 
  return { port, host };
}

Más verboso, sí. Pero el error de tipeo del ejemplo anterior ahora es un error de compilación, no una llamada a las 2 a.m. Ese intercambio — unas líneas extra en el límite a cambio de corrección en todo lo que sigue — casi siempre vale la pena.

Técnicas de narrowing que no se sienten como trabajo extra

La fricción con unknown desaparece en cuanto tienes un pequeño kit de patrones de narrowing que reutilizas en lugar de reinventar en cada punto de llamada.

Los type guards son la opción más limpia cuando la verificación es reutilizable:

tstypescript
interface User {
  id: string;
  email: string;
}
 
function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    typeof (value as Record<string, unknown>).id === "string" &&
    typeof (value as Record<string, unknown>).email === "string"
  );
}
 
function greet(value: unknown) {
  if (!isUser(value)) {
    throw new Error("Expected a User");
  }
  // value is narrowed to User from this line onward
  console.log(`Hello, ${value.email}`);
}

Las librerías de validación en tiempo de ejecución eliminan por completo el guard manual una vez que las formas se vuelven lo bastante complejas como para que las verificaciones escritas a mano sean propensas a errores. Zod es el patrón al que recurro con más frecuencia:

tstypescript
import { z } from "zod";
 
const UserSchema = z.object({
  id: z.string(),
  email: z.string().email(),
});
 
type User = z.infer<typeof UserSchema>;
 
function parseUser(payload: unknown): User {
  // throws with a descriptive error if payload doesn't match
  return UserSchema.parse(payload);
}

Es la misma idea que el type guard manual, pero el esquema es también tu única fuente de verdad tanto para la verificación en tiempo de ejecución como para el tipo en tiempo de compilación — sin riesgo de que ambos se desalineen.

Dónde aparece realmente unknown en código real

Tres lugares concentran la mayor parte del any que encontrarás en un código base existente, y los tres tienen un reemplazo directo basado en unknown:

UbicaciónUso típico de anyReemplazo con unknown
Bloques catchcatch (err: any)catch (err: unknown), acotar con instanceof Error
Resultados de JSON.parseretorno implícito anyanotar : unknown, validar con un esquema
Respuestas de APIs externasfetch(...).then(r => r.json()) tipado como anytipar el retorno del wrapper de fetch como unknown

TypeScript 4.4 resolvió la primera fila dejando de convertirla en un problema, al permitir tipar los errores capturados como unknownlas notas de la versión explican el razonamiento detrás de por qué any era el valor por defecto antes de eso y por qué no era una buena idea:

tstypescript
// ❌ err is any — every property access is unchecked
try {
  await saveUser(user);
} catch (err: any) {
  console.error(err.message); // works, but also compiles if err is a string
}
 
// ✅ err is unknown — you have to prove it's an Error first
try {
  await saveUser(user);
} catch (err: unknown) {
  const message = err instanceof Error ? err.message : String(err);
  console.error(message);
}

JSON.parse es la misma historia en miniatura: su tipo de retorno es any por defecto en los tipos de la biblioteca estándar, lo que significa que en el momento en que lo llamas, has reintroducido silenciosamente todos los problemas que unknown se suponía que resolvía. Envuélvelo una vez:

tstypescript
function parseJson(text: string): unknown {
  return JSON.parse(text);
}

y cada punto de llamada se ve obligado a validar antes de poder usar el resultado.

Cuándo any sigue siendo la decisión correcta

unknown no es una regla que se aplique en todas partes sin excepción. any todavía tiene algunos usos legítimos:

  • Librerías de terceros con definiciones de tipos faltantes o rotas, como una vía de escape documentada y aislada — no esparcida por todo tu propio código.
  • Migración gradual de un código base grande en JavaScript, donde any es un punto de paso deliberado y temporal hacia tipos más estrictos.
  • Restricciones genéricas en casos poco frecuentes donde unknown forzaría un casting innecesario en código que es demostrablemente seguro por construcción (una utilidad interna bien probada, por ejemplo).

La diferencia entre estos casos y el ejemplo de parseConfig de antes es la intención. any como decisión consciente y acotada está bien. any como opción por defecto porque nadie quiso escribir un type guard es de donde vienen los bugs.

Puntos clave

  1. any desactiva la verificación de tipos para un valor y todo lo que se deriva de él — los errores aparecen en tiempo de ejecución en lugar de en tiempo de compilación.
  2. unknown acepta cualquier valor pero exige acotarlo antes de usarlo, y eso es lo que lo hace seguro.
  3. Los type guards reutilizables (value is Type) mantienen el narrowing conciso en lugar de repetir verificaciones en cada lugar.
  4. La validación basada en esquemas (Zod o similar) es la herramienta correcta cuando las formas se vuelven lo bastante complejas como para que los guards escritos a mano corran el riesgo de desalinearse con la realidad.
  5. Los bloques catch, los resultados de JSON.parse y las respuestas de APIs externas son los tres lugares donde any se cuela con más frecuencia — reemplaza los tres con unknown más un paso de acotamiento.
  6. Reserva any para excepciones documentadas — código de terceros sin tipos o un paso de migración deliberado — no como opción por defecto cuando escribir un type guard resulta incómodo.
Wilfredo Rujel

Wilfredo Rujel

Ingeniero de Software Full Stack

Compartir esta publicaciónX