Sustituyendo `any` de TypeScript: seis patrones seguros de tipos para casos comunes
Aprenda alternativas prácticas y seguras desde el punto de vista tipológico al tipo `any` de TypeScript, como unknown, generics, uniones discriminadas y verificaciones exhaustivas, para manejar datos impredecibles.
Fuentes: Stop Using ‘any’ in TypeScript — Here’s the Right Fix Every Time
Un pequeño cambio en la disciplina que te protege de sorpresas en producción
Cada desarrollador de TypeScript ha pasado por esto. Es tarde un viernes por la tarde, un extraño error del compilador está reteniendo tu solicitud de integración, y después de veinte minutos luchando con generics anidados y tipos union, lo único que deseas es entregar el código y apagar tu portátil.
Entonces recurrís a esas tres letras que resuelven todo y, al mismo tiempo, nada:
const payload: any = response.data;
La línea subrayada en rojo desaparece. La compilación muestra color verde. Todo parece estar bien.
Pero eso no está bien. Usar any de ninguna manera resuelve un problema de escritura; solo le indica a TypeScript que deje de prestar atención. Has desactivado precisamente esa red de seguridad que te hizo elegir TypeScript en primer lugar. Cada vez que aparece any, se crea un vacío en tu cobertura de tipos, y ese vacío se extiende silenciosamente a todas las funciones y variables que entran en contacto con él.
La buena noticia es que casi nunca lo necesitas realmente. A continuación se explica por qué any causa tanto daño, seguido de alternativas comprobadas para las situaciones en las que los desarrolladores recurren a él con mayor frecuencia.
Qué significa realmente any (y por qué tiene efectos negativos)
Muchos desarrolladores asumen que any simplemente significa “el tipo de este valor aún no se conoce”.
Lo que en realidad se comunica al compilador es: “Deje de verificar esta variable y confíe en que todo lo que haga con ella a partir de ahora esté correcto.”
let user: any = { name: "Alice" };
// TypeScript allows all of these without a single warning:
user.authenticate(); // Runtime crash: user.authenticate is not a function
user.address.postalCode; // Runtime crash: Cannot read properties of undefined
user(); // Runtime crash: user is not a function
Lo que empeora la situación son las expansiones any. Una vez que el tipo de retorno de una función es any, toda variable que capture ese valor de retorno también se convierte en any. Al pasar ese valor a otra función, la pérdida de seguridad continúa propagándose. Pronto, grandes partes de su código base se convierten efectivamente en JavaScript puro con una extensión .ts.
Las secciones siguientes explican la alternativa correcta para cada caso común en el que any tiende a aparecer.
Solución 1: Cuando el tipo realmente aún no se conoce (unknown)
La situación: Estás extrayendo datos de una API externa, analizando una cadena JSON obtenida del almacenamiento local o manejando un payload sin procesar proveniente de un webhook. En estos casos, no hay forma de conocer la estructura exacta de los datos antes de que el programa se ejecute realmente.
El enfoque problemático:
function parseApiResponse(jsonString: string): any {
return JSON.parse(jsonString);
}
const data = parseApiResponse('{"id": 101}');
console.log(data.profile.avatar); // Compiles fine, crashes immediately if profile is missing
La solución correcta: Utiliza unknown junto con un type guard.
unknown funciona como una versión más segura de any. Le indica al compilador: “El tipo de este valor aún no está determinado, pero no se permite acceder a sus propiedades, llamar a sus métodos ni operar sobre él hasta que se demuestre cuál es en realidad”.
interface UserProfile {
id: number;
name: string;
}
// Custom type guard
function isUserProfile(value: unknown): value is UserProfile {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
typeof (value as Record<string, unknown>).id === "number" &&
"name" in value &&
typeof (value as Record<string, unknown>).name === "string"
);
}
function parseApiResponse(jsonString: string): unknown {
return JSON.parse(jsonString);
}
const rawData = parseApiResponse('{"id": 101, "name": "Sarah"}');
// rawData.name; // Error: Object is of type 'unknown'.
if (isUserProfile(rawData)) {
// Inside this block, TypeScript knows rawData is definitely UserProfile
console.log(rawData.name); // Fully typed, autocompleted, and safe!
}
Si consideras que crear validaciones manualmente para cargas de trabajo complejas implica demasiado esfuerzo, puedes utilizar una biblioteca de validación en tiempo de ejecución como Zod o Valibot. Estas bibliotecas validan la entrada desconocida y determinan el tipo correspondiente en TypeScript en un solo paso.
Solución 2: Cuando una función necesita soportar varios tipos (generics)
La situación: Estás creando una utilidad reutilizable — como un envoltorio para caché, una herramienta para arrays o un método en un cliente API — diseñada para funcionar con diversas estructuras de datos.
El enfoque problemático:
function getFirstItem(items: any[]): any {
return items[0];
}
const numbers = [1, 2, 3];
const firstNum = getFirstItem(numbers); // firstNum is typed as 'any'
firstNum.toUpperCase(); // Compiles! Crashes at runtime because it's a number.
La solución correcta: Utiliza generics (la sintaxis <T>).
Los genéricos funcionan como marcadores de posición para tipos en lugar de valores. En lugar de descartar la información del tipo como lo hace any, un genérico recuerda el tipo proporcionado por quien lo llama y lo mantiene hasta el resultado que devuelve la función.
function getFirstItem<T>(items: T[]): T | undefined {
return items[0];
}
const numbers = [1, 2, 3];
const firstNum = getFirstItem(numbers);
// TypeScript automatically infers firstNum as: number | undefined
const names = ["Alice", "Bob"];
const firstName = getFirstItem(names);
// TypeScript automatically infers firstName as: string | undefined
También puedes restringir qué tipo puede ser un genérico utilizando extends, asegurando así que el tipo incluya ciertas propiedades requeridas:
interface HasId {
id: string | number;
}
function printId<T extends HasId>(entity: T): void {
console.log(entity.id);
}
Solución 3: Manejo de errores dentro de try / catch
La situación: Estás escribiendo lógica dentro de un bloque catch para manejar algo que salió mal.
El enfoque problemático:
try {
fetchUserData();
} catch (err: any) {
// Risky: In JavaScript, anything can be thrown (strings, numbers, null)
console.error(err.message.toUpperCase());
}
El riesgo aquí es que JavaScript permite lanzar literalmente cualquier cosa: cadenas, números, null, objetos simples, no solo instancias de Error. Si alguna biblioteca o un fallo inesperado lanza una cadena simple, como throw "Network timeout", entonces err.message será undefined, y llamar a métodos como .toUpperCase() sobre él puede generar un error nuevo e sin manejar justo dentro del código destinado a tratar errores.
La solución correcta: Declare el error capturado como unknown y filtrelo utilizando instanceof Error.
try {
fetchUserData();
} catch (err: unknown) {
if (err instanceof Error) {
// TypeScript safely knows 'err' has a .message property
console.error(`Error: ${err.message}`);
} else {
// Fallback for non-standard thrown objects
console.error("An unexpected error occurred", String(err));
}
}
Solución 4: Objetos dinámicos y búsquedas por clave
La situación: Necesita un objeto que se comporte como un mapa o diccionario, con claves que solo son conocidas mientras el programa está en ejecución.
El enfoque problemático:
const userRolePermissions: any = {};
userRolePermissions["admin"] = ["read", "write", "delete"];
La solución correcta: Utilizar Record<Keys, Values>, o alternativamente una firma indexada.
type Role = "admin" | "editor" | "viewer";
type Permission = "read" | "write" | "delete";
// A strictly defined key-value mapping
const rolePermissions: Record<Role, Permission[]> = {
admin: ["read", "write", "delete"],
editor: ["read", "write"],
viewer: ["read"],
};
// If the keys can be arbitrary dynamic strings:
const sessionCache: Record<string, unknown> = {};
Usar Record<string, unknown> mantiene la estructura del objeto verificable, al tiempo que obliga a quien lo utilice a comprobar qué tipo de valor es antes de manipularlo.
Solución 5: Indicar cambios en la estructura (Uniones discriminadas)
La situación: Un proceso pasa por fases distintas — como inactivo, cargando, éxito y error — y cada fase contiene su propio tipo de datos.
El enfoque problemático:
interface RequestState {
status: any;
data?: any;
error?: any;
}
Modelar esto con una única interfaz flexible significa que cada propiedad se vuelve opcional, obligándote a utilizar cadenas de ?. en todo tu código. Peor aún, nada impide que llegues a un estado absurdo en el que tanto data como error se llenen al mismo tiempo.
La solución correcta: Utiliza uniones discriminadas.
Dale a cada estado posible un campo literal compartido —comúnmente llamado status o type— que TypeScript pueda usar para distinguir las variantes:
type RequestState =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: string[] }
| { status: "error"; error: Error };
function renderUI(state: RequestState) {
switch (state.status) {
case "idle":
return "Click start";
case "loading":
return "Loading...";
case "success":
// TypeScript automatically knows state.data exists here!
return `Loaded ${state.data.length} items`;
case "error":
// TypeScript automatically knows state.error exists here!
return `Failed: ${state.error.message}`;
}
}
Solución 6: Verificación exhaustiva con never
La situación: Deseas que el propio compilador indique cuando un compañero de equipo agrega un nuevo caso a un tipo de unión pero olvida actualizar todas las cadenas switch o if/else que lo manejan.
El enfoque problemático: Recurrir a una solución genérica default: return any, o simplemente no tratar ese caso adicional en absoluto.
La solución correcta: Asignar cualquier valor restante al tipo never.
never describe un valor que lógicamente no puede ocurrir. Una vez que la instrucción switch haya cubierto todos los miembros de la unión, lo que quede en la rama default se reduce a never. Si alguien posteriormente extiende la unión con una nueva variante, el compilador marca inmediatamente un error en esa línea, porque el valor restante ya no coincide con never:
type NotificationType = "email" | "sms" | "push";
function sendNotification(type: NotificationType) {
switch (type) {
case "email":
// Handle email
break;
case "sms":
// Handle SMS
break;
case "push":
// Handle push notification
break;
default: {
// If someone adds "in_app" to NotificationType and forgets to update this function,
// TypeScript will raise a compile-time error right here.
const _exhaustiveCheck: never = type;
return _exhaustiveCheck;
}
}
}
Referencia rápida: Sustituir any
Cómo implementarlo de forma consistente en todo el equipo
Comprender estos reemplazos solo resuelve la mitad del problema; la otra mitad consiste en evitar que any vuelva a colarse en el momento en que se acerca la fecha límite.
Integre esta restricción directamente en la configuración de su proyecto para que se aplique automáticamente:
En tsconfig.json:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true
}
}
En su configuración de ESLint (.eslintrc.json):
{
"rules": {
"@typescript-eslint/no-explicit-any": "error"
}
}
El objetivo principal de TypeScript es brindarte confianza: confianza al refactorizar, confianza al desplegar, y autocompletado del editor que te ahorra ir constantemente a la documentación. Eliminar any puede parecer restrictivo durante los primeros días. Pero una vez que unknown, los genericos y las uniones discriminadas toman su lugar, tu código comienza a explicarse por sí mismo, los fallos en tiempo de ejecución disminuyen drásticamente y el compilador finalmente funciona como la protección para la que fue diseñado.
Lecturas relacionadas
- Bucles agencia limitados: Patrones confiables de TypeScript para el uso en herramientas de LLM — Aprende cómo arquitecturar agentes de TypeScript para LLM utilizando bucles agencia limitados con validación Zod, ejecución determinista de herramientas y recuperación de errores para lograr una fiabilidad de nivel profesional.