Inicio / Artículos

Errores comunes en JavaScript y TypeScript que dañan silenciosamente el código

Trampas en JavaScript y TypeScript que parecen correctas pero aún causan errores: comparaciones con NaN, coerción de tipos, problemas de sincronización con async/await y comprobaciones de igualdad que te engañan.

Editor: REACTAPP.TOP9 min de lectura1973 palabras

Fuentes: 20 JavaScript/TypeScript Mistakes That Look Right… But Break Your App

Trampas de comparación e igualdad

1. NaN nunca es igual a nada, ni siquiera a sí mismo.

[1, NaN, 3].indexOf(NaN); // -1, even though NaN is right there

Según la especificación, NaN === NaN devuelve false; por lo tanto, cualquier mecanismo basado en igualdad estricta —incluido indexOf— fallará al intentar localizarlo, incluso cuando se encuentra directamente en el array. Utilice Number.isNaN(value) en su lugar, o bien .includes(), que fue diseñado específicamente para manejar correctamente este caso límite.

2. typeof null devuelve "object".

if (typeof value === "object") { value.someMethod(); } // value could be null

Se trata de una antigua peculiaridad inherente al lenguaje, que engaña a quienes consideran que typeof es una forma fiable de determinar si algo es un objeto. null pasa esa comprobación sin problemas, pero genera un error en el instante en que se intenta llamar a algún método en él.

3. La comparación de objetos mediante JSON.stringify() es sensible al orden.

JSON.stringify({ a: 1, b: 2 }) === JSON.stringify({ b: 2, a: 1 }); // false

Dos objetos que representan los mismos datos pueden serializarse en cadenas de texto con las claves en un orden diferente; esto ocurre naturalmente después de acceder a una base de datos o a otra parte del código. Evite comparaciones basadas en cadenas y utilice una herramienta adecuada para verificar la igualdad profunda.

4. .sort() sin un comparador trata todo como texto.

[10, 2, 1].sort(); // [1, 10, 2]

Los valores se convierten primero en cadenas y luego se comparan carácter por carácter, lo que distorsiona el orden numérico. Siempre proporcione una función comparadora explícita para números, como (a, b) => a - b.

Coerción de tipos

5. El operador + pasa a la concatenación en cuanto uno de los operandos es una cadena.

function total(price, quantity) {
  return price + quantity; // "5" + 3 = "53" if price came from a form input
}

Los valores que provienen de parámetros de consulta, campos de formulario o JSON analizado suelen permanecer como cadenas aunque parezcan números. + no convertirá la cadena en un número por usted; en cambio, convierte el número en una cadena.

6. Los valores por defecto en la desestructuración solo se aplican cuando la propiedad es undefined, nunca para null.

function greet({ name = "friend" } = {}) { return `Hi, ${name}`; }
greet({ name: null }); // "Hi, null" — the default never kicks in

Passar explícitamente null se considera como proporcionar un valor, no como omitirlo, por lo que el valor predeterminado de respaldo se evita por completo. Si los llamantes podrían enviar legítimamente null, es necesario verificarlo por separado.

Timing asíncrono

7. Ejecutar .filter(Boolean) sobre un array de promesas no sirve para nada.

const results = items.map((item) => checkAsync(item)); // array of Promise objects
const valid = results.filter(Boolean); // every Promise is truthy, filters nothing

Una instancia de Promise siempre es verdadera, independientemente de lo que finalmente resuelva o rechace. Es necesario resolverlas primero con await Promise.all(results), y solo entonces filtrar los valores resultantes.

8. La cadena opcional detiene la evaluación de toda la expresión, no solo de la parte opcional.

user?.getPermissions().includes("admin"); // if user is null, this is undefined, not an error, but also never calls .includes at all

Es tentador pensar que solo se omite el segmento ?., pero todo lo que viene después también deja de ejecutarse. Ese suele ser el comportamiento deseado, aunque se convierte en un error sutil cuando una llamada final, como una de registro o análisis, nunca se ejecuta en silencio.

9. Promise.all descarta todos los resultados tan pronto como una sola promesa falla.

const [a, b, c] = await Promise.all([taskA(), taskB(), taskC()]);
// if taskC rejects, you get nothing back from taskA and taskB either, even if they succeeded

Esto es intencional: un comportamiento de todo-o-nada. Cuando el éxito parcial es aceptable en su situación, Promise.allSettled devuelve el resultado de cada promesa, ya sea exitosa o fallida, en lugar de descartar los resultados exitosos porque una tarea falló.

Mutación de objetos y arrays

10. const bloquea el binding, no el objeto al que apunta.

const config = { retries: 3 };
config.retries = 10; // completely legal, const never protected this

Declarar algo con const solo impide reasignar esa variable a un valor diferente. El objeto al que hace referencia sigue siendo tan mutable como si se hubiera usado let. Si necesita inmutabilidad real, utilice Object.freeze(); tenga en cuenta que solo congela un nivel de profundidad.

11. .sort() muta el array original, a diferencia de .map() o .filter().

const original = [3, 1, 2];
const sorted = original.sort();
console.log(original); // [1, 2, 3] — also changed, probably not what was intended

Es fácil asumir que todos los métodos de array devuelven un nuevo array, ya que la mayoría de los comunes hacen exactamente eso. .sort() y .reverse() son las excepciones, y mutar un array al que otra parte de su código aún tiene una referencia provoca el clásico error de “este valor cambió por sí solo”.

12. for...in también itera sobre las propiedades heredadas, no solo sobre las claves propias del objeto.

Array.prototype.customMethod = function () {};
const arr = [1, 2, 3];
for (const key in arr) { console.log(key); } // includes "customMethod" too

En la práctica esto rara vez causa problemas, ya que es poco común extender prototipos integrados, pero cualquier propiedad enumerable heredada de la cadena de prototipos aparecerá dentro de un bucle for...in. Utilizar Object.keys() o for...of evita por completo esta sorpresa.

Funciones y ámbito

13. Las funciones de flecha nunca vinculan su propio this.

const counter = {
  count: 0,
  increment: () => { this.count++; }, // `this` here is NOT the counter object
};

Una función de flecha toma this del ámbito léxico en el que fue escrita, y no del objeto que finalmente la llama. Esto las hace ideales para callbacks que necesitan conservar un this externo, pero son una mala opción para métodos de objeto o clase que deben hacer referencia a su propia instancia.

14. Una instrucción switch continúa ejecutándose silenciosamente cuando se omite break.

switch (status) {
  case "pending":
    console.log("Order is pending");
  case "shipped": // no break above, this runs too
    console.log("Order has shipped");
}

Si se omite un único break, todas las casas siguientes se ejecutarán, independientemente de si su condición coincide realmente con la entrada actual. Aunque a veces se utiliza intencionadamente este comportamiento, es tan raro que la ausencia de break casi siempre indica un error y no una decisión de diseño.

Fechas y números

15. 0.1 + 0.2 no es 0.3.

0.1 + 0.2 === 0.3; // false
0.1 + 0.2; // 0.30000000000000004

Esto se debe a la representación estándar de números en punto flotante, y afecta a casi todos los lenguajes, no es algo exclusivo de JavaScript. Evite utilizar la igualdad estricta con valores decimales calculados; en su lugar, verifique si la diferencia entre dos números está dentro de un margen de tolerancia aceptable.

16. Las cadenas de fechas sin formato se interpretan como UTC, mientras que las fechas separadas por barra se procesan en el tiempo local.

new Date("2024-01-01");   // midnight UTC
new Date("01/01/2024");   // midnight local time

Dos cadenas que parecen describir el mismo día pueden corresponder a fechas calendáricas diferentes según la zona horaria de quien ejecute el código. Elija un formato de fecha y consústelo siempre, y utilice una biblioteca de fechas bien establecida en lugar de analizar las cadenas manualmente.

Atrapas específicas de TypeScript

17. Las aserciones de tipo no validan nada en tiempo de ejecución.

const user = JSON.parse(rawData) as User; // TypeScript trusts you, nothing is actually checked

Escribir as User le indica al compilador que confíe plenamente en usted, y eso es exactamente lo que hace, sin realizar ninguna verificación de si el valor analizado realmente tiene la estructura que usted afirmó. Los datos defectuosos o inesperados siguen fluyendo por su programa etiquetados como verificados, hasta que eventualmente causan un error en algún punto posterior.

18. El operador de afirmación no nula solo suprime advertencias, no cambia la realidad.

function getName(user?: User) {
  return user!.name; // TypeScript stops warning you; the runtime null check is still gone
}

Agregar ! le indica a TypeScript que estás seguro de que un valor no es ni null ni undefined. Si esa suposición es incorrecta, el compilador simplemente deja de señalar el riesgo, y el error aparece en tiempo de ejecución, justo donde el sistema de tipos solía detectarlo.

19. La verificación de propiedades excesivas solo se aplica a los literales de objeto pasados directamente, no a las variables que tienen la misma estructura.

type Config = { retries: number };
function setup(config: Config) {}

setup({ retries: 3, timeout: 5000 }); // error: 'timeout' does not exist on type Config

const options = { retries: 3, timeout: 5000 };
setup(options); // no error — same excess property, but it slips through

Al pasar un literal de objeto directamente a una llamada a función, TypeScript examina cada propiedad y rechaza cualquier elemento adicional. Si primero se asigna ese mismo objeto a una variable y luego se pasa dicha variable, la verificación desaparece por completo. Es una peculiaridad real en el comportamiento del comprobador de tipos, lo que significa que un error tipográfico o un campo adicional puede pasar desapercibido en la validación, dependiendo únicamente de cómo llegó el valor a la función.

20. Los enums numéricos aceptan números arbitrarios, no solo sus miembros declarados.

enum Status { Pending, Shipped, Delivered }
function process(status: Status) {}

process(999); // no compile error, even though 999 isn't a declared Status value

En el fondo, los enums numéricos en TypeScript son simplemente números normales, por lo que el sistema de tipos aceptará cualquier valor numérico como válido, independientemente de si corresponde a un miembro declarado. Los enums de cadena imponen restricciones reales en este caso, así que cuando necesitas que el compilador rechace efectivamente las entradas inválidas, opta por enums de cadena o una unión formada a partir de literales de cadena.

Ninguno de estos veinte problemas es oscuro o exótico. Cada uno es lo suficientemente común como para aparecer en código real, revisado y desplegado, precisamente porque se asemeja perfectamente al JavaScript o TypeScript ordinario hasta que una entrada específica, un momento determinado o un caso límite lo revelan. Estar al tanto de esta lista no sustituye la necesidad de probar tu código cuidadosamente. Lo que te ofrece es una ventaja: terminas verificando los problemas de los que ya sabes que debes estar atento, en lugar de encontrártelos después en producción.

Lectura relacionada

  • Modelado de dominios en TypeScript: Más allá de las anotaciones de tipos básicas — Aprenda hábitos prácticos en TypeScript, desde la diferencia entre “unknown” y “any”, pasando por uniones discriminadas y el operador “satisfies”, que le ayudarán a modelar estados válidos en lugar de simplemente etiquetar datos.
  • Tratar el contenido como código: Un pipeline de semillado de Git a Postgres — Muestra cómo reemplazar un CMS por JSON controlado por Git, validación con Zod y operaciones upsert de Prisma para sembrar de forma segura contenido estructurado en Postgres.
  • Cinco errores engañosos de Node.js que superan la revisión de código sin ser detectados — Se analizan cinco errores reales en Node.js relacionados con forEach, promesas flotantes y copias superficiales para comprender por qué el código que funciona bien puede fallar aún así en producción.
  • Concepciones comunes sobre async/await que causan errores en producción — Se explican nueve malentendidos sutiles sobre async/await, desde condiciones de carrera hasta rechazos no manejados, que dañan silenciosamente las aplicaciones JavaScript en el mundo real.
  • Domina los tipos de utilidad integrados de TypeScript para un código más limpio — Aprende cómo tipos de utilidad como Partial, Pick, Omit y Record eliminan las interfaces duplicadas y mantienen automáticamente sincronizadas las definiciones de tipo.