Inicio / Artículos / Siete características nativas de JavaScript que reemplazan las dependencias de utilidades.

Siete características nativas de JavaScript que reemplazan las dependencias de utilidades.

How Temporal, al utilizar declaraciones, inserciones en Map, Math.sumPrecise, ayudantes de iterador, métodos de Set y Promise.withResolvers, elimina el código genérico común en JavaScript.

2067 palabras

Antes, iniciar un proyecto serio en JavaScript significaba instalar una biblioteca de fechas, una herramienta para manejar colecciones y un complemento para agrupar datos, y luego envolver cada recurso utilizado en bloques try...finally para protegerlo. En los últimos ciclos de lanzamiento, una serie de propuestas del TC39 que se debatieron durante años se han convertido en parte del estándar y ya están disponibles en navegadores permanentes, Node.js, Bun y Deno. Esta guía explica siete de ellas, el problema que resuelve cada una y qué considerar antes de eliminar una dependencia.

Una nota sobre el soporte antes de comenzar: estas funcionalidades llegaron a los motores en diferentes momentos, y algunas son muy recientes. Verifique los datos de compatibilidad actuales para los navegadores y versiones de runtime que utilice, y mantenga un polyfill o la biblioteca antigua cuando el soporte aún no esté garantizado.

Temporal: una verdadera API de fechas y horas

Cualquiera que haya utilizado el objeto integrado Date durante más de unos minutos ya se ha topado con sus problemas:

  • Los meses están indexados a partir de cero (0 corresponde a enero y 11 a diciembre), pero los días del mes comienzan en 1.
  • Las instancias de Date son mutables, por lo que pasar una a una función auxiliar puede cambiar silenciosamente el valor del llamante.
  • La conversión de zonas horarias y los cálculos relacionados con el horario de verano requieren operaciones manuales complejas o la utilización de bibliotecas como date-fns, Day.js o Luxon.

Temporal es una nueva API inmutable para fechas y horas diseñada para reemplazar a Date. No elimina a Date del lenguaje, ya que el código existente sigue funcionando, pero el código nuevo ya no lo necesita.

Tipos separados para conceptos distintos

La decisión clave en el diseño de Temporal es modelar diferentes ideas como tipos separados:

  • Temporal.PlainDate: una fecha de calendario sin hora ni zona horaria, como un cumpleaños.
  • Temporal.PlainTime: una hora según el reloj de pared sin fecha, como una alarma a las 07:00.
  • Temporal.ZonedDateTime: un momento exacto vinculado a una zona horaria y calendario específicos.

El fragmento a continuación lee la hora actual en Nueva York, agrega 14 días a una fecha simple y mide la diferencia entre dos fechas en meses y días. Observe que la diferencia se solicita con largestUnit: 'month', lo que hace que el resultado se muestre como meses más días en lugar de un simple recuento de días.

// 1. Getting current time in a specific time zone
const meetingTime = Temporal.Now.zonedDateTimeISO('America/New_York');
console.log(meetingTime.toString());
// e.g. "2026-09-04T12:09:28-04:00[America/New_York]"

// 2. Safe, readable date math
const deadline = Temporal.PlainDate.from('2026-09-01');
const followUp = deadline.add({ days: 14 });

console.log(followUp.toString()); // "2026-09-15"

// 3. Calculating the exact difference between two dates
const start = Temporal.PlainDate.from('2026-01-01');
const end = Temporal.PlainDate.from('2026-09-04');
const diff = start.until(end, { largestUnit: 'month' });

console.log(`${diff.months} months and ${diff.days} days`);
// "8 months and 3 days"

Cada objeto Temporal es inmutable. Llamar a add() nunca modifica el original; devuelve un nuevo objeto. Para muchos proyectos, eso significa que una biblioteca de fechas puede quedar fuera del paquete. Antes de migrar, revise trampas comunes de Temporal.

Gestión explícita de recursos con using

Una fuente frecuente de fugas y bloqueos es olvidarse de cerrar algo que se abrió:

  • un handle de archivo que permanece abierto después de la lectura
  • una conexión a base de datos que queda pendiente cuando una ruta API lanza un error
  • listeners de eventos o hilos de trabajo que nunca se detienen

La solución tradicional es try...finally. Al tener tres recursos en una función, esto se convierte rápidamente en código genérico profundamente anidado. La gestión explícita de recursos incluye la declaración using junto con dos símbolos bien conocidos: Symbol.dispose y Symbol.asyncDispose.

Cómo se activa la eliminación

Cuando un valor se declara con using, el motor llama a su método [Symbol.dispose]() tan pronto como la ejecución abandona el bloque contenedor, ya sea que el bloque termine normalmente o debido a que se haya lanzado un error. Si varios recursos se declaran en un mismo ámbito, se eliminan en orden inverso a su declaración, lo cual es exactamente lo que harían los bloques anidados try...finally.

En el ejemplo, DatabaseSession implementa el gancho de eliminación, y runReport declara una sesión con using. El orden del registro muestra que la limpieza se ejecuta después de la consulta, al finalizar la función:

// Define a resource that knows how to clean itself up
class DatabaseSession {
  constructor(dbName) {
    this.connection = `Connected to ${dbName}`;
    console.log("Database opened.");
  }

  query(sql) {
    return `Results for: ${sql}`;
  }

  // Built-in disposal hook
  [Symbol.dispose]() {
    console.log("Database connection closed automatically!");
  }
}

// Using the resource
function runReport() {
  using session = new DatabaseSession("AnalyticsDB");

  const data = session.query("SELECT * FROM metrics");
  console.log(data);

  // When runReport finishes (or if it throws),
  // session[Symbol.dispose]() runs immediately.
}

runReport();
// Logs:
// 1. "Database opened."
// 2. "Results for: SELECT * FROM metrics"
// 3. "Database connection closed automatically!"

Para una limpieza asíncrona, como cerrar un socket de red, implemente [Symbol.asyncDispose]() y declare el recurso con await using dentro de una función asíncrona. El entorno de ejecución luego esperará a que se realice la limpieza antes de continuar.

Actualizaciones y inserciones en Map con getOrInsert y getOrInsertComputed

Map es la estructura adecuada para colecciones claveadas, pero el patrón “léelo o créalo si falta” siempre ha sido torpe. Agrupar valores bajo una clave suele verse así:

// The old way: multiple lookups and manual branching
if (!userCache.has(userId)) {
  userCache.set(userId, fetchDefaultProfile(userId));
}
const profile = userCache.get(userId);

Eso implica dos búsquedas (has seguido de get) más una estructura condicional, solo para garantizar que exista un elemento. Los nuevos métodos lo reducen a una sola llamada y una única búsqueda:

  • map.getOrInsert(key, defaultValue): devuelve el valor existente para key, o almacena defaultValue bajo esa clave y lo devuelve.
  • map.getOrInsertComputed(key, callback): es similar, pero la función de callback se ejecuta únicamente cuando la clave falta, por lo que nunca se genera innecesariamente un valor por defecto costoso.

Dicha distinción es importante en el ejemplo anterior. getOrInsert(userId, fetchDefaultProfile(userId)) llamaría a fetchDefaultProfile en cada acceso, ya que los argumentos de la función se evalúan antes de la llamada; la variante calculada evita esto. El siguiente fragmento agrupa los eventos según los IDs de usuario, creando un array vacío solo la primera vez que aparece un usuario:

const userActivity = new Map();

// Grouping events under user IDs
function logEvent(userId, eventName) {
  // If userId doesn't exist, create an empty array, insert it, and return it.
  const events = userActivity.getOrInsertComputed(userId, () => []);
  events.push(eventName);
}

logEvent("user_42", "login");
logEvent("user_42", "clicked_button");

console.log(userActivity.get("user_42"));
// ['login', 'clicked_button']

Math.sumPrecise para totales precisos

Los números de JavaScript siguen el estándar IEEE 754 de punto flotante de doble precisión, lo que genera el famoso resultado a continuación:

0.1 + 0.2; // 0.30000000000000004

En facturas, carritos o agregaciones de gráficos, tales errores se acumulan hasta que el total resulta claramente incorrecto.

Math.sumPrecise() toma un iterable de números y calcula su suma como si se tuviera precisión ilimitada, redondeando solo una vez al final. Un reduce ingenuo redondea después de cada paso, y de ahí proviene la desviación. En el ejemplo, los cuatro ítems de la factura suman 0.9999999999999999 con reduce, mientras que Math.sumPrecise devuelve 1 (la consola muestra 1, no 1.0):

const invoiceItems = [0.1, 0.2, 0.3, 0.4];

// Old reduce approach:
const naiveTotal = invoiceItems.reduce((acc, n) => acc + n, 0);
console.log(naiveTotal);
// 0.9999999999999999

// New Math.sumPrecise approach:
const accurateTotal = Math.sumPrecise(invoiceItems);
console.log(accurateTotal);
// 1.0

Es importante comprender este límite. Math.sumPrecise elimina el error de redondeo acumulado, pero no puede cambiar el hecho de que valores como 0.1 no son representables con exactitud en formato binario. Sumar solo [0.1, 0.2] sigue dando como resultado 0.30000000000000004, ya que la suma exacta de esos dos valores de tipo double se redondea a ese valor. Esto representa una verdadera mejora para las estadísticas y los paneles de control; sin embargo, en cuestiones monetarias, las unidades menores enteras (centavos) o una biblioteca de números decimales siguen siendo la opción más segura.

Ayudas para iteradores e Iterator.concat

Durante más de diez años, los arrays han ofrecido .map(), .filter() y .slice(), mientras que los iteradores y generadores, como las secuencias infinitas o los cursores de base de datos, no contaban con nada comparable. Para usar .map() en un generador que produce miles de filas, era necesario primero convertir todo en un array con [...generator()], lo cual anulaba la ventaja de memoria que ofrece el streaming.

Los auxiliares para iteradores añaden métodos como map, filter, take, drop y flatMap directamente a los iteradores, y Iterator.concat() une varios iteradores en uno solo. Todos ellos son perezosos: un elemento se calcula únicamente cuando el consumidor lo solicita.

El ejemplo conecta auxiliares a un generador infinito. Dado que la evaluación es perezosa y take(3) se detiene después de tres valores, el bucle termina aunque la fuente nunca termine:

function* infiniteCounter() {
  let count = 1;
  while (true) {
    yield count++;
  }
}

// Grab an iterator from our infinite generator
const stream = infiniteCounter()
  .filter(num => num % 2 === 0)   // Keep even numbers
  .map(num => `Count: ${num}`)     // Format them
  .take(3);                        // Stop after 3 values

for (const item of stream) {
  console.log(item);
}
// Logs:
// "Count: 2"
// "Count: 4"
// "Count: 6"

Para unir varias secuencias sin crear un array intermedio, páselas a Iterator.concat(). Este consume cada iterador por turnos:

const firstBatch = [1, 2, 3].values();
const secondBatch = [4, 5, 6].values();

const combined = Iterator.concat(firstBatch, secondBatch);

console.log([...combined]); // [1, 2, 3, 4, 5, 6]

Los auxiliares de Iterator ya estaban disponibles en los motores principales desde hace tiempo; Iterator.concat() es más reciente, así que úsalo por separado.

Operaciones nativas de Set

Durante mucho tiempo, un Set ofrecía poco más que .has(), .add() y .delete(). Cualquier operación como la intersección requería convertirlo a arrays y filtrarlos manualmente:

// The old manual way:
const intersection = new Set([...setA].filter(x => setB.has(x)));

Set.prototype ahora incluye la verdadera álgebra de conjuntos: union, intersection, difference, symmetricDifference, además de los predicados isSubsetOf, isSupersetOf y isDisjointFrom. Cada operación devuelve un nuevo Set sin modificar los originales.

El ejemplo compara dos conjuntos de roles, extrayendo solo los permisos que tienen los administradores y verificando que los roles del editor son un subconjunto de los del administrador:

const adminRoles = new Set(['read', 'write', 'delete', 'audit']);
const editorRoles = new Set(['read', 'write']);

// Find privileges exclusive to admins
const adminOnly = adminRoles.difference(editorRoles);
console.log([...adminOnly]); // ['delete', 'audit']

// Check role containment
console.log(editorRoles.isSubsetOf(adminRoles)); // true

También omiten los arrays temporales que crea el enfoque de despliegue y filtrado.

Promise.withResolvers para promesas diferidas

A veces, una promesa debe resolverse desde fuera de su constructor, por ejemplo mediante un manejador de eventos o una función de callback registrada en otro lugar. Ese patrón “deferrido” solía requerir que las variables se extrajeran del ejecutor:

// The clunky way
let resolveFn, rejectFn;
const myPromise = new Promise((res, rej) => {
  resolveFn = res;
  rejectFn = rej;
});

Promise.withResolvers() devuelve la promesa junto con sus funciones resolve y reject en un único objeto. En el ejemplo, un manejador de clics resuelve la promesa y otro código simplemente la espera. Tenga en cuenta que el fragmento utiliza await de nivel superior, por lo que debe ejecutarse en un módulo ES o dentro de una función async:

const { promise, resolve, reject } = Promise.withResolvers();

// Attach your listeners or pass the resolve handle to an event handler
document.getElementById("submit-btn").addEventListener("click", () => {
  resolve("User clicked submit!");
}, { once: true });

// Await the promise wherever you need it
const message = await promise;
console.log(message);

Este patrón es útil para colas de eventos personalizadas, conexiones con Web Workers y para adaptar APIs basadas en callbacks o emisores a código async.

Qué significa esto para sus dependencias

El hilo conductor es la paridad: el lenguaje ahora abarca tareas rutinarias como las fechas, la limpieza de recursos, el agrupamiento por clave y las operaciones matemáticas en conjuntos que antes se delegaban a bibliotecas del entorno del usuario. Antes de agregar otro paquete en tu próximo sprint, revisa tus objetivos:

  • Si distribuyes tu código en navegadores modernos o en versiones actuales de Node.js, Bun o Deno, muchas de estas herramientas ya existen a nivel global.
  • Cada dependencia que evitas significa un paquete más pequeño, un inicio de ejecución más rápido y código que cualquier desarrollador de JavaScript puede leer sin necesidad de aprender una API de terceros.

Puntos clave

  • Temporal proporciona tipos inmutables y claramente separados para las fechas; reemplaza a Date en nuevo código sin eliminarlo.
  • using y await using hacen que la limpieza sea automática y ordenada, sustituyendo a los bloques anidados try...finally.
  • Preferir getOrInsertComputed cuando el valor por defecto es costoso de crear.
  • Math.sumPrecise corrige la desviación acumulada por redondeo, no la representación binaria; mantenga las cantidades monetarias como enteros.
  • Los auxiliares de iterador y los métodos de Set eliminan categorías completas de conversiones de array.
  • Verifique el soporte en tiempo de ejecución para cada funcionalidad, ya que estas propuestas se implementaron en plazos muy diferentes.
  • Lecturas relacionadas