L'enregistrement de données chronologiques qui doivent rester identiques, quel que soit l'emplacement de l'utilisateur (par exemple, les dates de naissance, les alarmes quotidiennes, les dates d'expiration des cartes de crédit ou les cycles de facturation mensuels), est historiquement sujet aux erreurs avec l'ancien objet Date. Étant donné qu'un Date représente toujours un instant précis et s'affiche en fonction du fuseau horaire local de l'utilisateur, l'enregistrement d'une date telle que "1990-01-01" peut entraîner l'affichage de "1989-12-31" pour les utilisateurs situés à l'ouest du fuseau horaire UTC. De plus, la modélisation de dates partielles (comme l'expiration d'une carte, qui n'indique que le mois) force l'utilisation de jours fictifs arbitraires qui provoquent des bugs de dépassement.
L'API Temporal fournit une famille de types "simples" indépendants de l'emplacement, qui ne comportent pas de décalage de fuseau horaire et représentent les dates et les heures telles qu'elles sont lues sur un calendrier ou une horloge :
| Type | Ce qu'il modélise | Cas d'utilisation courants |
|---|---|---|
Temporal.PlainDate |
Date complète (year, month, day) |
Dates de naissance, dates d'événements, dates de facturation des abonnements |
Temporal.PlainTime |
Temps réel (hour, minute, second, etc.) |
Alarmes quotidiennes (9h, où que se trouve l'utilisateur), horaires d'ouverture |
Temporal.PlainDateTime |
Date de calendrier + heure de l'horloge sans fuseau horaire | Planifications locales flottantes, combinant un PlainDate et un PlainTime avant d'associer une zone |
Temporal.PlainMonthDay |
Mois et jour sans l'année | Anniversaires récurrents, fêtes fixes |
Temporal.PlainYearMonth |
Année et mois sans jour | Expirations de cartes de crédit (2027-12), périodes de facturation mensuelles |
Implémentation
- Choisissez le type
Temporal.Plain*le plus précis qui correspond à vos données :- Utilisez
Temporal.PlainDate.from()pour les dates complètes du calendrier etTemporal.PlainTime.from()pour les heures de l'horloge. - Utilisez
Temporal.PlainDateTime.from()(ouplainDate.toPlainDateTime(plainTime)) lorsque vous combinez une date calendaire et une heure sans les lier à un fuseau horaire. - Utilisez
Temporal.PlainMonthDay.from()pour les dates annuelles sans année, et convertissez-les enPlainDateavec.toPlainDate({ year })avant d'effectuer des calculs de date. - Utilisez
Temporal.PlainYearMonth.from()pour les valeurs d'année et de mois afin que les numéros de jour arbitraires ne soient jamais pris en compte dans les calculs.
- Utilisez
- Notez le paramètre
calendarlors de la construction de types Plain : ISO 8601 ('iso8601') est le calendrier par défaut lors de l'analyse de chaînes ISO ou de la construction avecPlainDate.from({ year, month, day }). Lorsque vous construisezPlainMonthDayouPlainYearMonthà partir de sacs de propriétés, ou lorsque vous travaillez avec des calendriers non ISO, transmettez explicitementcalendar(par exemple,{ month: 10, day: 31, calendar: 'iso8601' }). - Utilisez
.add()avec les optionsoverflowpour les intervalles d'agenda récurrents : l'appel deplainDate.add({ months: 1 })utilise par défaut{ overflow: 'constrain' }, ce qui limite les dates de fin de mois comme le 31 janvier au 28/29 février. Transmettez{ overflow: 'reject' }lorsque les dates cibles non valides doivent générer uneRangeErrorau lieu d'être limitées.
Exemple : dates complètes, heures de l'horloge et 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;
Exemple : Dates partielles (PlainMonthDay et 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;
Exemple : Intervalles mensuels récurrents (overflow: 'constrain' vs '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
}
Exemple : Interopérabilité avec l'ancien Date
Un Date hérité est un code temporel (un instant), et non une date calendaire. Le stockage de "1990-01-01" en tant que Date vous oblige à choisir un instant (généralement minuit UTC), et sa lecture dans un fuseau horaire à l'ouest de l'UTC donne "31/12/1989". Si une bibliothèque ou une API nécessite toujours un Date, effectuez une conversion explicite via Temporal.Instant au lieu de créer des chaînes ISO manuellement :
// 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
Implémentation stratégique et bonnes pratiques
- À FAIRE : Utilisez
Temporal.PlainDatepour les dates complètes du calendrier,Temporal.PlainTimepour les heures,Temporal.PlainDateTimepour les paires date-heure locales sans fuseau horaire,Temporal.PlainMonthDaypour les dates annuelles récurrentes etTemporal.PlainYearMonthpour les dates d'expiration ou les périodes de facturation exprimées en année-mois. - N'EFFECTUEZ PAS d'opérations arithmétiques directement sur
Temporal.PlainMonthDay. Convertissez-le d'abord enTemporal.PlainDatevia.toPlainDate({ year }), car la durée des mois varie d'une année à l'autre. - À FAIRE : Utilisez
.add()surTemporal.PlainDatepour les intervalles de calendrier récurrents, en vous appuyant suroverflow: 'constrain'(par défaut) pour limiter les dates de fin de mois ou suroverflow: 'reject'pour générer uneRangeErrorsur les dates inexistantes. - À FAIRE : Spécifiez
calendar(par exemple,calendar: 'iso8601') lorsque vous construisez des typesPlainMonthDayou Plain non ISO à partir d'objets. - N'UTILISEZ PAS de types simples si vous devez représenter un moment spécifique dans le temps physique (un instant). Utilisez
Temporal.InstantouTemporal.ZonedDateTimepour les journaux, les codes temporels d'événements ou la planification sur plusieurs fuseaux horaires. - NE construisez PAS d'objets
Dateà partir de chaînes ISO créées manuellement (comme`${date}T00:00:00Z`). Produisez unDateà partir de laepochMillisecondsd'unTemporal.Instantou d'unTemporal.ZonedDateTime, et lisez-en un avecTemporal.Instant.fromEpochMilliseconds(date.getTime())oudate.toTemporalInstant(), afin que l'hypothèse de fuseau horaire soit explicite. - NE PAS utiliser
Intl.DateTimeFormat.prototype.formatToParts()pour extraire ou comparer des valeurs d'année, de mois ou de jour. Lisez.year,.monthet.daydirectement à partir d'un type Plain, puis comparez-les avec.equals(). - À FAIRE : Identifiez les fuseaux horaires par leur nom IANA (par exemple,
America/New_York) et jamais par un décalage UTC fixe. Les décalages changent avec l'heure d'été et la législation. Par conséquent, les libellés tels que"UTC-5"deviennent incorrects pendant une partie de l'année. - À FAIRE : N'oubliez pas que les objets
Temporalsont immuables. Les méthodes telles que.add(),.subtract()ou.with()renvoient toujours une nouvelle instance avec une nouvelle référence au lieu de modifier l'objet d'origine sur place. Elles peuvent donc être stockées directement dans l'état réactif de l'UI.
Stratégie de remplacement
Solutions de remplacement et compatibilité des navigateurs avec Temporal
Pour les navigateurs qui ne sont pas encore compatibles avec l'API Temporal native, utilisez la détection de fonctionnalités et chargez dynamiquement temporal-polyfill (~15 Ko min+gz).
Notez que l'importation de temporal-polyfill n'attribue pas automatiquement l'objet Temporal au champ d'application global. Attribuez-la explicitement si votre code repose sur 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();
})();