Temporal API en JavaScript: cómo usar las fechas que por fin funcionan (y migrar desde Date)
Durante treinta años la parte rota de JavaScript han sido las fechas. Temporal llegó a Stage 4 en el plenario de TC39 de marzo de 2026, ya se puede usar sin banderas y Node.js 26 lo trae activado por defecto: aquí van las recetas concretas y cómo migrar sin reescribir tu proyecto.
Qué es Temporal y qué arregla de verdad
Temporal es el reemplazo del objeto Date que llevaba nueve años en el proceso de estandarización de JavaScript. No es una librería más que hay que instalar: es una API del lenguaje, con tipos distintos para cosas que Date mezclaba en un solo objeto, y con inmutabilidad en todas sus operaciones.
Los cinco problemas de Date, uno por uno
- Los meses van de 0 a 11, así que
new Date(2026, 8, 25)es septiembre y no agosto: un error de un mes en la mitad de los formularios del mundo. - Los objetos son mutables: un
setMonth()cambia el objeto que otras partes del código estaban usando, sin que nadie lo haya pedido. - No existe un tipo "solo fecha" ni un tipo con zona horaria real. Todo se representa como un instante medido en la zona del sistema.
- El parseo de cadenas depende del motor:
new Date('2026-09-25')se interpreta como UTC mientrasnew Date('2026/09/25')se interpreta como hora local. - La aritmética se hace sumando milisegundos, así que sumar 24 horas no equivale a sumar un día cuando ese día cambia el horario de verano.
Temporal resuelve los cinco con tipos inmutables, meses numerados desde 1, tipos con y sin zona, construcción explícita y aritmética consciente del calendario.
Elegir el tipo correcto: Instant, ZonedDateTime, PlainDate, PlainTime, PlainDateTime y Duration
El 80% de los errores al empezar es elegir mal el tipo. La regla es simple: si representa un momento del tiempo en el mundo, necesitas zona; si representa una fecha de calendario, no.
Lee también
Temporal.Instant: un punto exacto en el tiempo, sin zona horaria (por ejemplo, el momento de un registro de auditoría).Temporal.ZonedDateTime: un instante con zona horaria explícita, el tipo correcto para agendas y recordatorios.Temporal.PlainDate,PlainTimeyPlainDateTime: fecha, hora o ambas sin zona, para cosas como un vencimiento o un horario de apertura.Temporal.Duration: una cantidad de tiempo (días, horas, minutos) que se puede sumar, comparar y expresar en la unidad que necesites.
Qué sigue haciendo Date (y seguirá haciendo)
Date no desaparece. Sigue siendo lo que devuelven y aceptan muchas APIs del navegador, la mayoría de librerías de terceros y varios formatos de serialización. En la frontera del sistema hay conversiones explícitas, y eso es sano: cada lado dice en qué tipo habla.
Estado real en septiembre de 2026: dónde funciona hoy
Un matiz que importa si vives de citar el estándar: en la tabla oficial de propuestas terminadas de TC39, consultada el 25 de septiembre de 2026, Temporal figura con publicación prevista para 2027, es decir la edición ES2027, junto a otras propuestas que también llegaron a Stage 4 este año. En la práctica esa fecha es lo de menos, porque las implementaciones van por delante del papel: el API lleva meses disponible en los motores.
Navegadores: Chrome y Firefox ya lo llevan, Safari todavía no
Según los datos de compatibilidad consultados el 25 de septiembre de 2026, Chrome lo soporta desde la versión 144, Firefox desde la 139 y Edge va en paralelo con Chrome, mientras que Safari solo lo tiene en Technology Preview y no en versiones estables ni en iOS. Caniuse calcula un 71,34% de uso global: suficiente para usarlo, insuficiente para ignorar el polyfill si tu público incluye Safari.
Node.js 26 lo activa por defecto y entra en LTS en octubre
La nota oficial de Node.js 26.0.0, publicada el 5 de mayo de 2026, incluye "the Temporal API enabled by default" entre los puntos destacados del release, junto con V8 14.6 y Undici 8.0. La versión pasa a LTS en octubre de 2026. Si tu backend ya corre Node 26, no hay nada que activar: Temporal está ahí. Deno lo incorporó desde la 2.7.
El polyfill oficial y cuándo merece la pena cargarlo
El polyfill se llama @js-temporal/polyfill y, a diferencia de otros, no instala un global: exporta su propio Temporal y una función toTemporalInstant.
npm install @js-temporal/polyfill
import { Temporal, toTemporalInstant } from '@js-temporal/polyfill';
Date.prototype.toTemporalInstant = toTemporalInstant;Eso evita pisar la implementación nativa donde ya existe, pero implica una decisión: si cargas el polyfill en un entorno que ya trae Temporal, tendrás dos implementaciones conviviendo y objetos de una no se comportan igual que los de la otra. Lo razonable es cargarlo solo cuando haga falta, por ejemplo comprobando si el global existe, y usar una sola fuente de Temporal en todo el código.
Recetas que se usan todos los días
La fecha de hoy en la zona del usuario
const hoy = Temporal.Now.plainDateISO(); // 2026-09-25, solo fecha
const ahora = Temporal.Now.zonedDateTimeISO(); // hora con la zona del sistema
const enMadrid = Temporal.Now.zonedDateTimeISO('Europe/Madrid');Fíjate en la diferencia: si lo que quieres es "la fecha de hoy para el usuario", plainDateISO() es lo correcto; si lo que quieres es saber qué hora es en un sitio concreto, necesitas un ZonedDateTime.
Sumar días, meses y años sin resultados raros
const vence = Temporal.PlainDate.from('2026-09-25').add({ days: 30 }); // 2026-10-25
const primeroDeMes = vence.with({ day: 1 }); // 2026-10-01Las operaciones devuelven un objeto nuevo: el original nunca cambia. with() cambia un campo concreto y reemplaza al viejo setDate, sin efectos colaterales sobre otras partes del código.
Cuántos días faltan: duraciones y diferencias entre fechas
const inicio = Temporal.PlainDate.from('2026-09-25');
const fin = Temporal.PlainDate.from('2026-12-31');
inicio.until(fin, { largestUnit: 'days' }).days; // 97
inicio.until(fin, { largestUnit: 'weeks' }).toString(); // P13W6Duntil() devuelve una Duration, no un número: puedes leer .days, pedir .total({ unit: 'days' }) para un valor decimal o quedarte con la representación en semanas y días. Para "cuánto falta" en días exactos, el largestUnit explícito te evita sorpresas.
Horario de verano: la aritmética que Date hacía mal
const reserva = Temporal.ZonedDateTime.from('2026-09-25T09:00[Europe/Madrid]');
reserva.add({ days: 1 }).hour; // 9: mantiene la hora de reloj
reserva.add({ hours: 24 }).hour; // 24 horas reales de tiempo transcurridoEn una zona con horario de verano, si entre esas dos fechas cambia la hora, los dos resultados difieren en una hora. Comprobado ejecutando el ejemplo en Europe/Madrid para el cambio de octubre de 2026: sumar un día mantiene las 09:00 y sumar 24 horas devuelve las 08:00. Sumar días conserva la hora local, que es lo que espera una agenda; sumar horas mide tiempo transcurrido. Ese es el bug clásico de Date con los milisegundos, resuelto en el tipo.
Convertir entre zonas horarias sin adivinar
const reunion = Temporal.ZonedDateTime.from('2026-09-25T15:00[Europe/Madrid]');
const enBogota = reunion.withTimeZone('America/Bogota');
enBogota.hour; // 8
enBogota.toString(); // 2026-09-25T08:00:00-05:00[America/Bogota]La conversión no toca el instante, cambia la zona con la que se muestra. Y el toString() incluye el desplazamiento y el nombre de la zona, así que lo que guardas en una API es inequívoco.
Hablar con el Date que ya tienes: milisegundos y toTemporalInstant()
const legado = new Date();
const instante = legado.toTemporalInstant(); // Date -> Instant
const vuelta = new Date(instante.epochMilliseconds); // Instant -> Date
const desdeMs = Temporal.Instant.fromEpochMilliseconds(1758800000000);toTemporalInstant() existe de forma nativa donde el motor trae Temporal y el polyfill lo agrega a Date.prototype si se lo asignas. Es la puerta de entrada más útil para migrar sin romper: la frontera sigue hablando Date y todo lo de dentro habla Temporal.
Mostrar la fecha en el idioma del usuario con Intl
const fecha = Temporal.PlainDate.from('2026-09-25');
fecha.toLocaleString('es-CL', { dateStyle: 'long' });
new Intl.DateTimeFormat('en-GB', { dateStyle: 'full' }).format(fecha);Temporal calcula y Intl presenta: el formateo sigue siendo el mismo que ya usabas. Separar esas dos responsabilidades es media batalla ganada en cualquier app con más de un idioma.
Migrar desde date-fns o dayjs sin reescribir todo
Equivalencias de las llamadas más frecuentes
addDays(fecha, 7)pasa afecha.add({ days: 7 }).differenceInDays(a, b)pasa ab.until(a, { largestUnit: 'days' }).days.startOfMonth(fecha)pasa afecha.with({ day: 1 }).isAfter(a, b)pasa aTemporal.PlainDate.compare(a, b) > 0.format(fecha, 'yyyy-MM-dd')pasa afecha.toString(), y para mostrar, atoLocaleString().
Estrategia: por puntos de uso, con una capa propia de utilidades
El error caro es reemplazar la librería entera de golpe. La ruta segura es distinta: crear una capa propia de utilidades (por ejemplo lib/fechas.js) que hoy envuelve a la librería y mañana devuelve objetos de Temporal, y migrar punto de uso por punto de uso, empezando por donde sí hay dolor: cálculos con zona horaria, diferencias entre fechas y todo lo que dependa del horario de verano. El resto del código no se entera, porque sigue llamando a tu capa.
Lo que conviene dejar como Date por ahora
Las entradas y salidas de APIs del navegador que esperan Date, la serialización ya existente y cualquier librería de terceros que pida Date en su firma: esas se envuelven, no se reescriben. El beneficio de quitar una dependencia no es un número mágico de kilobytes, es dejar de mantener y actualizar código que ya no necesitas.
Errores típicos al empezar
Comparar con === y ordenar con menor que: no funcionan como esperas
a === b compara referencias, así que dos objetos con la misma fecha dan false. Para el valor existe a.equals(b). Y con los operadores de orden pasa algo peor: el motor convierte el objeto a texto, y ahí las comparaciones se vuelven frágiles en cuanto entran zonas horarias distintas. Usa los métodos de comparación del propio tipo.
Confundir un PlainDateTime con un instante real
Un PlainDateTime no sabe en qué zona está: es "el 25 de septiembre a las 09:00" en algún lugar del mundo. Para registrar cuándo pasó algo, usa Instant o ZonedDateTime. Guardar un PlainDateTime como si fuera un momento concreto es la forma más rápida de perder una hora en producción.
Duplicar el global Temporal o cargar el polyfill en todas partes
Decide una sola condición de carga y respétala. Si el polyfill se activa donde ya hay Temporal nativo, vas a tener dos implementaciones y comparaciones entre objetos de una y otra, con resultados confusos. En TypeScript, además, comprueba la librería de tipos que trae tu versión y, si todavía no incluye Temporal, declara lo mínimo mientras llega.
Conclusión
Temporal ya no es una promesa: está en el estándar, en Node 26 por defecto y en Chrome, Edge y Firefox, con Safari pendiente y polyfill para cubrirlo. La decisión sensata no es migrar todo hoy, es empezar por los sitios donde Date te está costando dinero. Si quieres seguir por aquí, mira también cómo hacer interactividad sin escribir JavaScript con HTMX y, si estás empezando, cómo usar localStorage y sessionStorage o qué es JavaScript y para qué sirve.
Referencias: Temporal en MDN, compatibilidad en caniuse, nota de Node.js 26.0.0 y propuestas terminadas de TC39.


