Históricamente, el registro de datos cronológicos que deben permanecer idénticos independientemente de la ubicación del usuario, como fechas de nacimiento, alarmas diarias, vencimientos de tarjetas de crédito o ciclos de facturación mensuales, ha sido propenso a errores con el objeto Date heredado. Debido a que un Date siempre representa un instante específico en el tiempo y se muestra con la zona horaria local del usuario, guardar una fecha como "1990-01-01" puede hacer que los usuarios al oeste de UTC vean "1989-12-31", mientras que modelar fechas parciales (como un vencimiento de tarjeta solo de mes) fuerza días de marcador de posición arbitrarios que causan errores de desbordamiento.
La API de Temporal proporciona una familia de tipos "simples" independientes de la ubicación que no tienen un desplazamiento de zona horaria y representan las fechas del calendario y las horas del reloj de pared exactamente como se leen en un calendario o reloj:
| Tipo | Qué modela | Casos de uso típicos |
|---|---|---|
Temporal.PlainDate |
Fecha completa del calendario (year, month, day) |
Fechas de nacimiento, fechas de eventos y fechas de facturación de suscripciones |
Temporal.PlainTime |
Tiempo de reloj (hour, minute, second, etcétera) |
Alarmas diarias (9:00 a.m. dondequiera que esté el usuario), horario de atención |
Temporal.PlainDateTime |
Fecha del calendario + hora del reloj sin zona horaria | Programaciones locales flotantes, que combinan un PlainDate y un PlainTime antes de adjuntar una zona |
Temporal.PlainMonthDay |
Mes y día sin año | Cumpleaños, aniversarios y días festivos fijos que se repiten anualmente |
Temporal.PlainYearMonth |
Año y mes sin día | Vencimientos de tarjetas de crédito (2027-12), períodos de facturación mensuales |
Cómo implementarla
- Elige el tipo de
Temporal.Plain*más específico que coincida con tus datos:- Usa
Temporal.PlainDate.from()para fechas de calendario completas yTemporal.PlainTime.from()para horas de reloj. - Usa
Temporal.PlainDateTime.from()(oplainDate.toPlainDateTime(plainTime)) cuando combines una fecha de calendario y una hora de reloj sin vincularlas a una zona horaria. - Usa
Temporal.PlainMonthDay.from()para las fechas anuales sin año y conviértelas enPlainDatecon.toPlainDate({ year })antes de realizar operaciones aritméticas de fechas. - Usa
Temporal.PlainYearMonth.from()para los valores de año y mes, de modo que los números de día arbitrarios nunca se filtren en los cálculos.
- Usa
- Ten en cuenta el parámetro
calendarcuando construyas tipos simples: ISO 8601 ('iso8601') es el calendario predeterminado cuando se analizan cadenas ISO o se construye conPlainDate.from({ year, month, day }). Cuando construyasPlainMonthDayoPlainYearMontha partir de bolsas de propiedades, o cuando trabajes con calendarios que no sean ISO, pasacalendarde forma explícita (p.ej.,{ month: 10, day: 31, calendar: 'iso8601' }). - Usa
.add()con las opciones deoverflowpara intervalos de calendario recurrentes: Cuando se llama aplainDate.add({ months: 1 }), se usa de forma predeterminada{ overflow: 'constrain' }, lo que limita las fechas de fin de mes, como el 31 de enero, al 28 o 29 de febrero. Pasa{ overflow: 'reject' }cuando las fechas de destino no válidas deban arrojar unRangeErroren lugar de limitarse.
Ejemplo: Fechas completas, horas de reloj y PlainDateTime
// 1. Parse a calendar date and a wall-clock time from input strings
const birthdate = Temporal.PlainDate.from('1990-01-01');
const reminderTime = Temporal.PlainTime.from('09:00');
// Combine into a location-agnostic PlainDateTime without string interpolation
const localAppointment = birthdate.toPlainDateTime(reminderTime);
// 2. Display without time zone conversion; outputs the same date in any time zone
console.log(birthdate.toLocaleString('en-GB'));
console.log(localAppointment.toString()); // "1990-01-01T09:00:00"
// 3. Read calendar fields directly from the Plain type. A PlainDate already *is*
// a calendar date, so there is no need for the formatToParts() workaround that
// legacy Date code uses to recover year/month/day from an instant.
const { year, month, day } = birthdate;
Ejemplo: Fechas parciales (PlainMonthDay y PlainYearMonth)
// Annual recurring event (no year component)
// Include explicit calendar when constructing from an object
const annualHoliday = Temporal.PlainMonthDay.from({
month: 10,
day: 31,
calendar: 'iso8601',
});
const today = Temporal.Now.plainDateISO();
const isToday = annualHoliday.equals(today.toPlainMonthDay());
// Convert PlainMonthDay to a PlainDate with a specific year before doing arithmetic,
// because month lengths (such as February) depend on whether the year is a leap year.
const occurrenceThisYear = annualHoliday.toPlainDate({ year: today.year });
// Monthly expiration (no day component, preventing day-of-month overflow bugs)
const cardExpiry = Temporal.PlainYearMonth.from({
year: 2027,
month: 12,
calendar: 'iso8601',
});
const currentMonth = today.toPlainYearMonth();
const isExpired = Temporal.PlainYearMonth.compare(cardExpiry, currentMonth) < 0;
Ejemplo: Intervalos mensuales recurrentes (overflow: 'constrain' frente a 'reject')
const billingStart = Temporal.PlainDate.from('2024-01-31');
// Default overflow: 'constrain' clamps Jan 31 + 1 month to the last valid day of February
const nextBilling = billingStart.add({ months: 1 }, { overflow: 'constrain' });
console.log(nextBilling.toString()); // "2024-02-29" (2024 is a leap year)
// Use overflow: 'reject' to throw a RangeError when the target day does not exist
try {
billingStart.add({ months: 1 }, { overflow: 'reject' });
} catch (err) {
// RangeError: day 31 does not exist in February
}
Ejemplo: Interoperabilidad con Date heredado
Un Date heredado es una marca de tiempo (un instante), no una fecha de calendario. Almacenar "1990-01-01" como Date te obliga a elegir un instante (por lo general, la medianoche UTC), y leerlo en una zona horaria al oeste de UTC produce "31/12/1989". Si una biblioteca o API aún requiere un Date, conviértelo de forma explícita a través de Temporal.Instant en lugar de compilar cadenas ISO de forma manual:
// PlainDate -> Date: attach a time zone (time of day defaults to the start of
// that day) to get an Instant, then hand the epoch milliseconds to Date.
// 'UTC' is a deliberate choice and must be documented; any zone would do,
// but the stored instant depends on it.
const instant = birthdate.toZonedDateTime('UTC').toInstant();
const legacyDate = new Date(instant.epochMilliseconds);
// Date -> PlainDate: go back through Instant (legacyDate.toTemporalInstant()
// is equivalent), then project into an explicit time zone. The result depends
// on the zone, which is exactly the drift risk.
const viewedDate = Temporal.Instant.fromEpochMilliseconds(legacyDate.getTime())
.toZonedDateTimeISO('America/New_York')
.toPlainDate();
// Compare Plain types with .equals() (or Temporal.PlainDate.compare), not strings.
console.log(viewedDate.equals(birthdate)); // false: the calendar date drifted to 1989-12-31
Implementación estratégica y prácticas recomendadas
- SÍ usa
Temporal.PlainDatepara fechas de calendario completas,Temporal.PlainTimepara horas de reloj,Temporal.PlainDateTimepara pares de fecha y hora locales sin zona,Temporal.PlainMonthDaypara fechas anuales recurrentes yTemporal.PlainYearMonthpara vencimientos de año y mes o períodos de facturación. - NO realices operaciones aritméticas directamente en
Temporal.PlainMonthDay; primero, conviértelo en unTemporal.PlainDatea través de.toPlainDate({ year })porque la duración de los meses varía según el año. - SÍ usa
.add()enTemporal.PlainDatepara intervalos de calendario recurrentes, confiando enoverflow: 'constrain'(el valor predeterminado) para fijar las fechas de fin de mes ooverflow: 'reject'para arrojar unRangeErroren fechas inexistentes. - SÍ especifica
calendar(p.ej.,calendar: 'iso8601') cuando construyas tiposPlainMonthDayo Plain que no sean ISO a partir de objetos. - NO uses tipos simples si necesitas representar un momento específico en el tiempo físico (un instante). Usa
Temporal.InstantoTemporal.ZonedDateTimepara los registros, las marcas de tiempo de los eventos o la programación en diferentes zonas horarias. - NO construyas objetos
Datea partir de cadenas ISO creadas manualmente (como`${date}T00:00:00Z`). Produce unDatea partir deepochMillisecondsde unTemporal.InstantoTemporal.ZonedDateTime, y lee uno conTemporal.Instant.fromEpochMilliseconds(date.getTime())odate.toTemporalInstant(), de modo que la suposición de la zona horaria sea explícita. - NO uses
Intl.DateTimeFormat.prototype.formatToParts()para extraer o comparar valores de año, mes o día. Lee.year,.monthy.daydirectamente desde un tipo Plain y compáralos con.equals(). - SÍ identifica las zonas horarias por el nombre de la IANA (p.ej.,
America/New_York) y nunca por una diferencia horaria fija con respecto al UTC. Los desplazamientos cambian con el horario de verano y la legislación, por lo que las etiquetas como"UTC-5"se vuelven incorrectas durante parte del año. - SÍ recuerda que los objetos
Temporalson inmutables. Los métodos como.add(),.subtract()o.with()siempre devuelven una instancia nueva con una referencia nueva en lugar de mutar el objeto original en su lugar, lo que hace que sea seguro almacenarlos directamente en el estado de la IU reactiva.
Estrategia de resguardo
Resguardos y compatibilidad de navegadores para Temporal
Para los navegadores que aún no admiten la API de Temporal nativa, usa la detección de funciones y carga temporal-polyfill de forma dinámica (~15 KB min+gz).
Ten en cuenta que importar temporal-polyfill no asigna automáticamente el objeto Temporal al alcance global. Asígnalo de forma explícita si tu código depende de globalThis.Temporal.
// Check if Temporal is supported natively
(async () => {
if (typeof Temporal === 'undefined') {
// Load the polyfill conditionally
const module = await import('https://esm.sh/temporal-polyfill');
globalThis.Temporal = module.Temporal;
// Extend Date.prototype if needed
Date.prototype.toTemporalInstant = module.toTemporalInstant;
}
initializeApp();
})();