La API Temporal y el Futuro de las Fechas en JavaScript
10 min read

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

EscenarioDate (legacy)Temporal
Definición de zonaImplícita (local o UTC)Explícita (objeto TimeZone)
Aritmética DSTSuma milisegundos ciegosAjusta offset según reglas
Hora inexistenteAjuste silencioso e impredecibleConfigurable (disambiguation)
Hora repetidaDevuelve la primera ocurrenciaConfigurable (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:

PolyfillTamañoConformidadRecomendación
@js-temporal/polyfill~50 KB gzip100% de la specPruebas y validación
fullcalendar/temporal-polyfill~20 KB gzipSuficiente para producciónApps 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.