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.

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.
// ❌ 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 undefinedEl 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.
// ✅ 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:
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:
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ón | Uso típico de any | Reemplazo con unknown |
|---|---|---|
Bloques catch | catch (err: any) | catch (err: unknown), acotar con instanceof Error |
Resultados de JSON.parse | retorno implícito any | anotar : unknown, validar con un esquema |
| Respuestas de APIs externas | fetch(...).then(r => r.json()) tipado como any | tipar 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 unknown — las 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:
// ❌ 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:
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
anyes un punto de paso deliberado y temporal hacia tipos más estrictos. - Restricciones genéricas en casos poco frecuentes donde
unknownforzarí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
anydesactiva 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.unknownacepta cualquier valor pero exige acotarlo antes de usarlo, y eso es lo que lo hace seguro.- Los type guards reutilizables (
value is Type) mantienen el narrowing conciso en lugar de repetir verificaciones en cada lugar. - 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.
- Los bloques
catch, los resultados deJSON.parsey las respuestas de APIs externas son los tres lugares dondeanyse cuela con más frecuencia — reemplaza los tres conunknownmás un paso de acotamiento. - Reserva
anypara 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.


