La API Temporal y el Futuro de las Fechas en JavaScript
Resumen
La gestión del tiempo en JavaScript siempre ha sido un problema. El objeto Date, heredado de Java en 1995, arrastra defectos de diseño que generan bugs constantemente. La API Temporal (Stage 3 en TC39, ya disponible en Chrome 144 y Firefox 139) es la solución definitiva: tipos inmutables, zonas horarias reales, precisión de nanosegundos y soporte para calendarios internacionales.
Este artículo analiza por qué Date falla, cómo Temporal lo resuelve, y qué necesitas para migrar.
1. El problema con Date
1.1 Un error con historia
En mayo de 1995, Brendan Eich creó JavaScript en 10 días. Para manejar fechas, copió directamente java.util.Date de Java 1.0. El problema: Java mismo reconoció que esa clase era defectuosa y la reemplazó dos años después. JavaScript, sin embargo, quedó atado a ella por compatibilidad con la web.
1.2 Defectos principales de Date
Mutabilidad
Date es mutable. Cualquier método set* modifica el objeto original, lo que causa bugs difíciles de rastrear cuando múltiples partes del código comparten la misma referencia:
const fecha = new Date('2026-03-15T10:00:00');
const copia = fecha;
copia.setFullYear(2030);
console.log(fecha.getFullYear()); // 2030 — ¡la original también cambió!
Para evitarlo, hay que hacer copias defensivas constantemente:
const copiaSegura = new Date(fecha.getTime());
Parsing inconsistente
Date.parse() interpreta las cadenas de forma diferente según el navegador y el motor:
const fecha = new Date('2025-01-01');
// En un navegador: se interpreta como UTC (medianoche UTC)
// En otro: se interpreta como hora local (medianoche local)
// Resultado: posible error de "un día de diferencia"
Sin aritmética temporal
No hay forma nativa de sumar “un mes” correctamente. El desarrollador debe hacerlo manualmente:
const fecha = new Date('2026-01-31');
fecha.setMonth(fecha.getMonth() + 1);
console.log(fecha.toISOString()); // 2026-03-03 — saltó febrero completo
Sin zonas horarias
Date solo conoce dos contextos: UTC y la zona local del sistema. No puede representar “10:00 AM en Tokio” desde un equipo en Nueva York sin trucos con offsets que fallan durante cambios de horario de verano.
1.3 La respuesta de TC39
El comité TC39 diseñó Temporal como un espacio de nombres completamente nuevo, libre de las restricciones de Date. Su diseño incorpora lecciones de Moment.js, Luxon, date-fns, java.time y NodaTime. El objetivo: hacer que escribir código incorrecto sea difícil por diseño.
2. Conceptos fundamentales de Temporal
2.1 Tiempo exacto vs. tiempo de reloj
La distinción más importante en Temporal:
-
Tiempo exacto: un punto preciso en la línea temporal universal, independiente de la ubicación. Dos eventos en el mismo instante son simultáneos sin importar si ocurrieron en Londres o Sídney. Se modela con
Temporal.Instant. -
Tiempo de reloj (wall-clock): lo que muestra un calendario o reloj local. “15 de marzo a las 09:00” no define un momento exacto hasta que se asocia con una zona horaria. Se modela con las clases
Plain*.
Date intentaba ser ambos a la vez y fallaba en los dos. Temporal obliga a ser explícito.
2.2 Inmutabilidad
Todas las clases de Temporal son inmutables. Métodos como .add(), .subtract() o .with() siempre devuelven una nueva instancia:
const hoy = Temporal.PlainDate.from('2026-02-10');
const mañana = hoy.add({ days: 1 });
console.log(hoy.toString()); // 2026-02-10 — sin cambios
console.log(mañana.toString()); // 2026-02-11
Esto elimina bugs de mutación, facilita la depuración y permite detección eficiente de cambios en frameworks reactivos como React o Vue.
2.3 Precisión de nanosegundos
Date opera con milisegundos (10⁻³ s). Temporal usa nanosegundos (10⁻⁹ s), seis órdenes de magnitud más preciso. Para manejar estos valores sin perder precisión numérica, Temporal usa BigInt:
const ahora = Temporal.Now.instant();
console.log(ahora.epochNanoseconds); // 1739188800000000000n (BigInt)
3. Tipos de datos de la API
3.1 Temporal.Instant
Representa un punto exacto en la línea temporal: nanosegundos desde la Época Unix. Sin zona horaria, sin calendario.
Uso típico: timestamps en bases de datos, logs de servidor, ordenación cronológica.
const ahora = Temporal.Now.instant();
// Desde un Date legacy
const legacyDate = new Date();
const instant = legacyDate.toTemporalInstant();
// Serialización: siempre en UTC
console.log(ahora.toString()); // 2026-02-10T16:26:10.123456789Z
Un Instant no puede mostrar información humana (día de la semana, mes) sin proyectarse en una zona horaria.
3.2 Temporal.ZonedDateTime
La clase más completa. Combina un instante exacto + zona horaria + calendario.
const reunion = Temporal.ZonedDateTime.from({
year: 2026,
month: 3,
day: 15,
hour: 10,
minute: 0,
timeZone: 'America/Lima',
});
console.log(reunion.toString());
// 2026-03-15T10:00:00-05:00[America/Lima]
// Consciente de cambios de horario de verano
const proximoDia = reunion.add({ days: 1 });
Características clave:
- Consciente de las reglas de horario de verano (DST)
- Usa identificadores IANA (
America/Lima), no solo offsets - Formato RFC 9557 que preserva toda la información
3.3 Clases Plain: tiempo civil sin zona horaria
PlainDate
Fecha sin hora ni zona horaria. Ideal para cumpleaños, feriados, vencimientos:
const cumpleaños = Temporal.PlainDate.from('1995-04-15');
const diasHastaHoy = cumpleaños.until(Temporal.Now.plainDateISO()).total({ unit: 'day' });
PlainTime
Hora del día sin fecha ni zona. Ideal para horarios recurrentes:
const apertura = Temporal.PlainTime.from('09:00');
const cierre = Temporal.PlainTime.from('17:30');
const duracion = apertura.until(cierre);
console.log(duracion.toString()); // PT8H30M
PlainDateTime
Fecha + hora, sin zona horaria. Equivale a “lo que dice el reloj en la pared”:
const evento = Temporal.PlainDateTime.from('2026-06-15T14:30');
const conZona = evento.toZonedDateTime('America/Bogota');
Nota: no usar para ordenar eventos globales. “2026-01-01 00:00” en Tokio ocurre antes que en Londres, pero sin zona horaria no se pueden comparar.
PlainYearMonth y PlainMonthDay
Tipos especializados para datos incompletos:
// Vencimiento de tarjeta: solo año y mes
const vencimiento = Temporal.PlainYearMonth.from('2028-10');
// Aniversario: solo mes y día
const navidad = Temporal.PlainMonthDay.from('12-25');
3.4 Temporal.Duration
Representa magnitudes de tiempo en componentes humanos. Sigue el formato ISO 8601:
const duracion = Temporal.Duration.from({ years: 1, months: 2, days: 10, hours: 2 });
console.log(duracion.toString()); // P1Y2M10DT2H
// La aritmética respeta el calendario
const enero31 = Temporal.PlainDate.from('2026-01-31');
const unMesDespues = enero31.add({ months: 1 });
console.log(unMesDespues.toString()); // 2026-02-28 — maneja correctamente los meses
// Comparar duraciones
const d1 = Temporal.Duration.from({ hours: 1, minutes: 30 });
const d2 = Temporal.Duration.from({ minutes: 90 });
console.log(Temporal.Duration.compare(d1, d2)); // 0 — son iguales
4. Zonas horarias y horario de verano (DST)
4.1 Identificadores IANA vs. offsets
Temporal distingue entre un identificador de zona (Europe/Madrid) y un offset numérico (+01:00):
// ❌ Peligroso: el offset cambia con DST
const conOffset = '2026-03-29T02:30:00+01:00';
// ✅ Seguro: Temporal recalcula el offset según las reglas de la zona
const zdt = Temporal.ZonedDateTime.from({
year: 2026,
month: 7,
day: 15,
hour: 10,
timeZone: 'Europe/Madrid',
});
console.log(zdt.offset); // +02:00 (verano)
4.2 Ambigüedades en transiciones DST
Cuando los relojes cambian, ocurren situaciones especiales:
- Hueco (gap): al adelantar relojes, una hora “no existe” (ej. de 02:00 salta a 03:00)
- Superposición (overlap): al atrasar relojes, una hora “ocurre dos veces”
Temporal gestiona esto con el parámetro disambiguation:
// Hora que no existe (hueco en España, último domingo de marzo)
const inexistente = Temporal.ZonedDateTime.from(
{ year: 2026, month: 3, day: 29, hour: 2, minute: 30, timeZone: 'Europe/Madrid' },
{ disambiguation: 'compatible' } // avanza a 03:30 (por defecto)
);
// Validación estricta: lanza error si la hora es ambigua
try {
Temporal.ZonedDateTime.from(
{ year: 2026, month: 3, day: 29, hour: 2, minute: 30, timeZone: 'Europe/Madrid' },
{ disambiguation: 'reject' }
);
} catch (e) {
console.log(e); // RangeError
}
4.3 Comparativa Date vs. Temporal en DST
| Escenario | Date (legacy) | Temporal |
|---|---|---|
| Definición de zona | Implícita (local o UTC) | Explícita (objeto TimeZone) |
| Aritmética DST | Suma milisegundos ciegos | Ajusta offset según reglas |
| Hora inexistente | Ajuste silencioso e impredecible | Configurable (disambiguation) |
| Hora repetida | Devuelve la primera ocurrencia | Configurable (earlier, later) |
5. Calendarios internacionales
Date solo soporta el calendario Gregoriano. Temporal integra soporte completo para calendarios internacionales, alineado con la API Intl.
5.1 Calendarios disponibles
Cada objeto PlainDate, PlainDateTime y ZonedDateTime incluye un campo calendar. Los calendarios soportados incluyen: hebrew, islamic, chinese, japanese, buddhist, persian, entre otros.
5.2 Aritmética según calendario
La operación “sumar un mes” tiene significados diferentes según el calendario:
// Calendario hebreo: puede tener 12 o 13 meses
const fechaHebrea = Temporal.PlainDate.from({
year: 5782,
month: 5,
day: 1,
calendar: 'hebrew',
});
const siguienteMes = fechaHebrea.add({ months: 1 });
// Calendario japonés con eras
const fechaJaponesa = Temporal.PlainDate.from({
era: 'reiwa',
eraYear: 8,
month: 2,
day: 10,
calendar: 'japanese',
});
console.log(fechaJaponesa.toString()); // 2026-02-10[u-ca=japanese]
5.3 Persistencia cultural
El calendario se preserva en la serialización:
const fecha = Temporal.PlainDate.from({
year: 5782,
month: 5,
day: 1,
calendar: 'hebrew',
});
console.log(fecha.toString()); // 2022-01-03[u-ca=hebrew]
// Al deserializar, se reconstruye con el calendario correcto
6. Serialización con RFC 9557
6.1 El problema de ISO 8601
El formato ISO 8601 (YYYY-MM-DDTHH:mm:ssZ) pierde información al convertir a UTC. Si las reglas de zona horaria cambian antes de un evento futuro, la hora guardada en UTC puede quedar desactualizada.
6.2 El formato RFC 9557
Temporal usa un formato extendido que preserva toda la información:
2026-03-15T10:00:00-05:00[America/Lima][u-ca=iso8601]
│ │ │ │ │
│ │ │ │ └─ Calendario
│ │ │ └──────────────── Zona horaria (fuente de verdad)
│ │ └───────────────────────── Offset (referencia rápida)
│ └────────────────────────────────── Hora civil (lo que ve el usuario)
└───────────────────────────────────────────────── Fecha
6.3 Resolución de conflictos
Si el offset almacenado no coincide con la zona horaria (por ejemplo, tras un cambio político), Temporal ofrece opciones:
const cadena = '2026-11-01T01:30:00-04:00[America/New_York]';
// use: confía en el offset (preserva instante exacto)
// ignore: confía en la zona horaria (preserva hora local)
// prefer: usa offset si es válido, si no recalcula
// reject: lanza error si hay contradicción
const zdt = Temporal.ZonedDateTime.from(cadena, { offset: 'prefer' });
7. Migración y ecosistema
7.1 Soporte en navegadores (2025-2026)
- Chrome / Edge: soporte completo desde la versión 144 (enero 2026)
- Firefox: soporte desde la versión 139 (mayo 2025)
- Safari: implementación activa en WebKit, disponible en Technical Preview
7.2 Polyfills
Para entornos sin soporte nativo:
| Polyfill | Tamaño | Conformidad | Recomendación |
|---|---|---|---|
@js-temporal/polyfill | ~50 KB gzip | 100% de la spec | Pruebas y validación |
fullcalendar/temporal-polyfill | ~20 KB gzip | Suficiente para producción | Apps sensibles al peso |
// Carga condicional del polyfill
if (!globalThis.Temporal) {
await import('@js-temporal/polyfill');
}
7.3 Temporal vs. librerías existentes
Temporal vs. Moment.js: Moment es mutable, pesado y está en modo mantenimiento. Temporal es inmutable, nativo y su sucesor directo.
Temporal vs. date-fns: date-fns opera sobre Date, heredando sus limitaciones. Probablemente evolucionará para ofrecer utilidades sobre objetos Temporal.
7.4 Guía de migración
La migración puede ser gradual. Temporal y Date coexisten:
// Paso 1: Convertir en los bordes de la aplicación
const legacyDate = new Date();
const instant = legacyDate.toTemporalInstant();
const zdt = instant.toZonedDateTimeISO('America/Bogota');
// Paso 2: Toda la lógica interna usa Temporal
const enUnaSemana = zdt.add({ weeks: 1 });
const soloFecha = zdt.toPlainDate();
// Paso 3: Convertir de vuelta solo cuando sea necesario
const deDateNuevo = new Date(enUnaSemana.epochMilliseconds);
8. Conclusión
Temporal no es una simple mejora de Date — es una corrección necesaria después de tres décadas. Tipos inmutables, zonas horarias reales, precisión de nanosegundos y soporte multicultural eliminan categorías enteras de bugs.
Con soporte nativo ya disponible en los principales navegadores en 2026, el momento de comenzar la transición es ahora. Empieza convirtiendo en los bordes, migra la lógica interna progresivamente, y planifica la eliminación de Date y sus librerías satélite.
Fuentes: propuesta oficial TC39, documentación MDN, análisis de implementación en navegadores y evaluaciones de polyfills comunitarios. Los ejemplos corresponden al estado Stage 3 vigente en febrero de 2026.