18. Temporal : le remplaçant moderne de Date
[Temporal] est un nouveau namespace global qui vise à remplacer l'ancien objet Date, jugé défaillant depuis longtemps : mutabilité, mois numérotés de 0 à 11, absence de vraie gestion des fuseaux horaires, analyse de chaînes ambiguë. Date n'est pas supprimé du langage — le code existant continue de fonctionner — mais Temporal est désormais l'API recommandée pour tout nouveau code manipulant dates, heures et durées.
18.1. ⚠️ Statut exact vis-à-vis d’ECMAScript 2026
La proposition Temporal a atteint le Stage 4 de TC39 en mars 2026, et de nombreux articles l'ont présentée comme faisant partie d'ECMAScript 2026. Le paragraphe officiel de la spécification résumant le contenu d'ECMAScript 2026 (cité au chapitre précédent) ne la mentionne cependant pas explicitement — le sujet reste à clarifier. Sur le plan pratique, Temporal est disponible nativement dans Node.js 26 et les navigateurs à jour, ce qui rend ce chapitre pertinent dès maintenant.
18.2. script [01-plainDate-plainTime-plainDateTime]
Ce script présente [Temporal.PlainDate] (une date sans heure ni fuseau), [Temporal.PlainTime] (une heure sans date), et [Temporal.PlainDateTime] (date + heure, sans fuseau) :
| 'use strict';
// ========================================================================
// [NOUVEAU ECMAScript 2026] Temporal : PlainDate, PlainTime, PlainDateTime
// ========================================================================
// [nécessite Node 26+ - voir temporal/README.md]
// [Temporal] est un tout nouveau namespace, pensé pour remplacer [Date],
// qui souffrait de plusieurs défauts : mutabilité, mois numérotés de 0 à 11
// (janvier = 0 !), absence de vraie gestion des fuseaux horaires, et un
// objet unique censé représenter à la fois une date, une heure ET un instant.
// Temporal sépare clairement ces notions en plusieurs types.
// ------------------------------------------------------------------------
// 1) Temporal.PlainDate : une date SANS heure ni fuseau horaire
// ------------------------------------------------------------------------
// utile pour : un anniversaire, une date d'échéance, un jour férié...
// des notions qui n'ont pas de "moment précis dans la journée"
const anniversaire = Temporal.PlainDate.from("2026-08-24");
console.log("anniversaire =", anniversaire.toString());
console.log("année =", anniversaire.year, ", mois =", anniversaire.month, ", jour =", anniversaire.day);
// contrairement à l'ancien Date, le mois est numéroté normalement : 8 = août (pas 7 !)
console.log("jour de la semaine (1=lundi..7=dimanche) =", anniversaire.dayOfWeek);
console.log("nombre de jours dans ce mois =", anniversaire.daysInMonth);
// ------------------------------------------------------------------------
// 2) immutabilité : add() et subtract() rendent un NOUVEL objet
// ------------------------------------------------------------------------
// avec l'ancien Date, date.setDate(date.getDate() + 7) modifie l'objet en place -
// une source classique de bugs quand cet objet est partagé ailleurs dans le code
const dansUneSemaine = anniversaire.add({ days: 7 });
console.log("anniversaire (inchangé) =", anniversaire.toString());
console.log("dansUneSemaine (nouvel objet) =", dansUneSemaine.toString());
// arithmétique sur les mois : gère automatiquement le nombre de jours du mois suivant
const finJanvier = Temporal.PlainDate.from("2026-01-31");
const unMoisPlusTard = finJanvier.add({ months: 1 });
console.log("finJanvier =", finJanvier.toString(), ", +1 mois =", unMoisPlusTard.toString());
// -> "2026-02-28" et non un débordement bizarre sur mars, contrairement à l'ancien Date
// ------------------------------------------------------------------------
// 3) comparaison de deux dates
// ------------------------------------------------------------------------
const rentrée = Temporal.PlainDate.from("2026-09-01");
console.log("anniversaire avant rentrée :", Temporal.PlainDate.compare(anniversaire, rentrée) < 0);
console.log("égalité :", anniversaire.equals("2026-08-24"));
// durée entre deux dates
const durée = anniversaire.until(rentrée);
console.log("durée jusqu'à la rentrée =", durée.toString());
// ------------------------------------------------------------------------
// 4) Temporal.PlainTime : une heure SANS date ni fuseau horaire
// ------------------------------------------------------------------------
// utile pour : "le cours commence à 14h30", indépendamment du jour
const heureDeCours = Temporal.PlainTime.from("14:30:00");
console.log("heureDeCours =", heureDeCours.toString());
const finDeCours = heureDeCours.add({ hours: 1, minutes: 30 });
console.log("finDeCours =", finDeCours.toString());
// ------------------------------------------------------------------------
// 5) Temporal.PlainDateTime : date + heure, mais SANS fuseau horaire
// ------------------------------------------------------------------------
// utile pour : un rendez-vous "civil" sans se soucier du fuseau (ex : un formulaire
// qui affiche juste "24/08/2026 14:30" sans préciser où)
const rendezVous = Temporal.PlainDateTime.from("2026-08-24T14:30:00");
console.log("rendezVous =", rendezVous.toString());
// on peut en extraire séparément la partie date et la partie heure
console.log("partie date =", rendezVous.toPlainDate().toString());
console.log("partie heure =", rendezVous.toPlainTime().toString());
|
- contrairement à l'ancien Date, le mois de Temporal.PlainDate est numéroté normalement : 8 désigne août, pas juillet ;
- [immutabilité] : add() et subtract() rendent systématiquement un nouvel objet — l'original n'est jamais modifié, contrairement à Date.prototype.setDate ;
- l'arithmétique sur les mois gère automatiquement les longueurs de mois différentes (31 janvier + 1 mois donne bien le 28 février, pas un débordement sur mars) ;
- Temporal.PlainDate.compare(...) et .until(...) permettent de comparer deux dates et de calculer une durée entre elles.
18.3. script [02-zonedDateTime]
[Temporal.ZonedDateTime] est le type le plus proche de l'ancien Date : il combine date, heure et fuseau horaire, de façon fiable et explicite :
| 'use strict';
// ========================================================================
// [NOUVEAU ECMAScript 2026] Temporal.ZonedDateTime : le vrai remplaçant de Date
// ========================================================================
// [nécessite Node 26+ - voir temporal/README.md]
// [Temporal.ZonedDateTime] est le type le plus proche de l'ancien [Date] :
// il combine une date, une heure ET un fuseau horaire - mais de façon fiable
// et explicite, là où [Date] gérait ça de façon confuse et implicite.
// ------------------------------------------------------------------------
// 1) création d'un ZonedDateTime, avec un fuseau horaire EXPLICITE
// ------------------------------------------------------------------------
// contrairement à "new Date('2026-08-24T14:30:00')" qui est ambigu (heure locale ?
// UTC ? ça dépend du format et du moteur JS !), ici le fuseau horaire est obligatoire
const réunionParis = Temporal.ZonedDateTime.from("2026-08-24T14:30:00[Europe/Paris]");
console.log("réunionParis =", réunionParis.toString());
console.log("fuseau =", réunionParis.timeZoneId, ", décalage =", réunionParis.offset);
// ------------------------------------------------------------------------
// 2) convertir la même réunion dans un autre fuseau horaire
// ------------------------------------------------------------------------
// withTimeZone() ne change PAS l'instant réel, seulement la façon de l'afficher -
// utile pour montrer l'heure d'un même événement à des participants dans le monde entier
const réunionNewYork = réunionParis.withTimeZone("America/New_York");
console.log("même réunion, vue depuis New York =", réunionNewYork.toString());
const réunionTokyo = réunionParis.withTimeZone("Asia/Tokyo");
console.log("même réunion, vue depuis Tokyo =", réunionTokyo.toString());
// ------------------------------------------------------------------------
// 3) l'arithmétique gère automatiquement les changements d'heure d'été (DST)
// ------------------------------------------------------------------------
// c'est LE piège classique de l'ancien Date : ajouter "24 heures" ne correspond pas
// toujours à ajouter "1 jour" lors d'un changement d'heure d'été/hiver
const avantChangementHeure = Temporal.ZonedDateTime.from("2026-10-24T20:00:00[Europe/Paris]");
// on ajoute un jour "calendaire" : Temporal comprend qu'il faut sauter le changement d'heure
const unJourPlusTard = avantChangementHeure.add({ days: 1 });
console.log("avant =", avantChangementHeure.toString());
console.log("+ 1 jour calendaire =", unJourPlusTard.toString());
// on ajoute exactement 24 heures : ce n'est PAS forcément la même heure locale
// si un changement d'heure d'été/hiver a eu lieu entretemps
const vingtQuatreHeuresPlusTard = avantChangementHeure.add({ hours: 24 });
console.log("+ 24 heures exactement =", vingtQuatreHeuresPlusTard.toString());
// ------------------------------------------------------------------------
// 4) l'heure actuelle, dans le fuseau horaire du système
// ------------------------------------------------------------------------
const maintenant = Temporal.Now.zonedDateTimeISO();
console.log("maintenant =", maintenant.toString());
console.log("fuseau du système =", Temporal.Now.timeZoneId());
// ------------------------------------------------------------------------
// 5) comparaison et durée entre deux ZonedDateTime dans des fuseaux différents
// ------------------------------------------------------------------------
const départAvion = Temporal.ZonedDateTime.from("2026-12-20T22:00:00[Europe/Paris]");
const arrivéeAvion = Temporal.ZonedDateTime.from("2026-12-21T11:30:00[Asia/Tokyo]");
// Temporal compare les instants réels, peu importe le fuseau affiché de chaque côté
const duréeVol = départAvion.until(arrivéeAvion);
console.log("durée du vol =", duréeVol.toString());
|
- contrairement à new Date('2026-08-24T14:30:00'), dont l'interprétation (heure locale ou UTC) dépend du format exact de la chaîne, Temporal.ZonedDateTime.from(...) exige un fuseau horaire explicite, entre crochets ;
- withTimeZone(...) convertit l'affichage d'un même instant réel dans un autre fuseau, sans changer l'instant lui-même — utile pour afficher l'heure d'une réunion à des participants dans plusieurs pays ;
- l'arithmétique gère correctement les changements d'heure d'été/hiver : ajouter « 1 jour calendaire » n'est pas toujours identique à ajouter « 24 heures exactement » — un piège classique de l'ancien Date.
18.4. script [03-duration-et-instant]
[Temporal.Duration] représente un laps de temps (pas un moment précis) ; [Temporal.Instant] représente un point précis et universel dans le temps — le remplaçant direct de new Date(...).getTime() :
| 'use strict';
// ========================================================================
// [NOUVEAU ECMAScript 2026] Temporal.Duration et Temporal.Instant
// ========================================================================
// [nécessite Node 26+ - voir temporal/README.md]
// ------------------------------------------------------------------------
// 1) Temporal.Duration : représente un LAPS de temps (pas un moment précis)
// ------------------------------------------------------------------------
// avant, une durée devait être bricolée "à la main" en millisecondes -
// peu lisible, et sans distinction entre unités calendaires (mois, années)
// et unités fixes (heures, minutes, secondes)
const duréeTrajet = Temporal.Duration.from({ hours: 2, minutes: 45 });
console.log("duréeTrajet =", duréeTrajet.toString());
console.log("heures =", duréeTrajet.hours, ", minutes =", duréeTrajet.minutes);
// on peut aussi créer une durée à partir de son écriture ISO 8601
const duréeChantier = Temporal.Duration.from("P3M2W"); // 3 mois et 2 semaines
console.log("duréeChantier =", duréeChantier.toString());
// total() convertit une durée entière dans une seule unité
console.log("duréeTrajet en minutes =", duréeTrajet.total("minutes"));
// ------------------------------------------------------------------------
// 2) additionner une durée à une date ou une heure
// ------------------------------------------------------------------------
const départ = Temporal.PlainTime.from("08:15:00");
const arrivée = départ.add(duréeTrajet);
console.log("départ =", départ.toString(), ", arrivée =", arrivée.toString());
// ------------------------------------------------------------------------
// 3) Temporal.Instant : un point PRÉCIS et UNIVERSEL dans le temps
// ------------------------------------------------------------------------
// contrairement à PlainDateTime ou ZonedDateTime (qui ont une notion de calendrier
// et de fuseau horaire), Instant représente juste "un instant", comme un timestamp -
// c'est le remplaçant direct de "new Date(quelqueChose).getTime()"
const maintenant = Temporal.Now.instant();
console.log("maintenant (Instant) =", maintenant.toString());
console.log("époque en millisecondes =", maintenant.epochMilliseconds);
// un Instant peut être converti vers un ZonedDateTime dans n'importe quel fuseau,
// puisqu'un instant précis correspond à une heure locale différente selon le fuseau
const maintenantAParis = maintenant.toZonedDateTimeISO("Europe/Paris");
const maintenantASydney = maintenant.toZonedDateTimeISO("Australia/Sydney");
console.log("maintenant à Paris =", maintenantAParis.toString());
console.log("maintenant à Sydney =", maintenantASydney.toString());
// ------------------------------------------------------------------------
// 4) durée entre deux instants, et comparaison
// ------------------------------------------------------------------------
const début = Temporal.Instant.from("2026-08-24T09:00:00Z");
const fin = Temporal.Instant.from("2026-08-24T17:30:00Z");
console.log("durée de la journée de travail =", début.until(fin).toString());
console.log("début avant fin :", Temporal.Instant.compare(début, fin) < 0);
|
- une Duration peut se construire à partir d'un objet ({ hours: 2, minutes: 45 }) ou d'une chaîne au format ISO 8601 ("P3M2W" pour 3 mois et 2 semaines) ;
- total(unité) convertit une durée entière dans une seule unité (par exemple, en minutes) ;
- un même Instant peut être converti en ZonedDateTime dans n'importe quel fuseau — un instant précis correspond à une heure locale différente selon où l'on se trouve.
18.5. script [04-comparaison-avec-Date]
Ce dernier script reprend, un par un, les défauts historiques de Date évoqués en introduction, et montre comment Temporal les corrige :
| 'use strict';
// ========================================================================
// [NOUVEAU ECMAScript 2026] Temporal vs Date : les pièges classiques corrigés
// ========================================================================
// [nécessite Node 26+ - voir temporal/README.md]
// [Date] n'est pas supprimé du langage (le code existant continue de fonctionner),
// mais [Temporal] est désormais l'API recommandée pour tout nouveau code.
// ce script illustre, un par un, les défauts historiques de [Date].
// ------------------------------------------------------------------------
// piège n°1 : les mois numérotés de 0 à 11 dans Date (janvier = 0 !)
// ------------------------------------------------------------------------
const dateAoût = new Date(2026, 7, 24); // 7 = août ?! très déroutant pour un débutant
console.log("[Date] mois affiché =", dateAoût.getMonth(), "(mais c'est bien le mois d'août)");
const plainDateAoût = Temporal.PlainDate.from({ year: 2026, month: 8, day: 24 });
console.log("[Temporal] mois affiché =", plainDateAoût.month, "(8 = août, comme tout le monde s'y attend)");
// ------------------------------------------------------------------------
// piège n°2 : Date est mutable - un objet partagé peut être modifié par erreur
// ------------------------------------------------------------------------
function ajouterUneSemaineDate(date: Date): Date {
date.setDate(date.getDate() + 7); // modifie l'objet reçu en paramètre !
return date;
}
const dateOriginale = new Date(2026, 7, 24);
const dateAvecUneSemaineEnPlus = ajouterUneSemaineDate(dateOriginale);
// piège : dateOriginale a aussi été modifiée, alors que ce n'était pas forcément voulu
console.log("[Date] dateOriginale après l'appel =", dateOriginale.toDateString(), "(modifiée !)");
console.log("[Date] dateAvecUneSemaineEnPlus =", dateAvecUneSemaineEnPlus.toDateString());
function ajouterUneSemaineTemporal(date: Temporal.PlainDate): Temporal.PlainDate {
return date.add({ days: 7 }); // rend un NOUVEL objet, ne modifie rien
}
const plainDateOriginale = Temporal.PlainDate.from({ year: 2026, month: 8, day: 24 });
const plainDateAvecUneSemaineEnPlus = ajouterUneSemaineTemporal(plainDateOriginale);
console.log("[Temporal] plainDateOriginale après l'appel =", plainDateOriginale.toString(), "(inchangée)");
console.log("[Temporal] plainDateAvecUneSemaineEnPlus =", plainDateAvecUneSemaineEnPlus.toString());
// ------------------------------------------------------------------------
// piège n°3 : l'analyse (parsing) d'une chaîne de caractères par Date est ambiguë
// ------------------------------------------------------------------------
// selon le format de la chaîne, "new Date(...)" l'interprète tantôt en heure locale,
// tantôt en UTC - un piège fréquent en production
console.log("[Date] new Date('2026-08-24') =", new Date("2026-08-24").toString(), "(interprété en UTC)");
console.log("[Date] new Date('2026-08-24 00:00:00') =", new Date("2026-08-24 00:00:00").toString(), "(interprété en heure locale)");
// -> les deux lignes ci-dessus peuvent afficher des heures différentes, pour la "même" date !
// avec Temporal, le comportement est toujours explicite et sans ambiguïté :
// un PlainDate n'a pas d'heure du tout, un ZonedDateTime exige un fuseau horaire précis
console.log("[Temporal] PlainDate.from('2026-08-24') =", Temporal.PlainDate.from("2026-08-24").toString());
// ------------------------------------------------------------------------
// piège n°4 : Date ne sait pas nativement calculer une durée "propre"
// ------------------------------------------------------------------------
const date1 = new Date(2026, 0, 1);
const date2 = new Date(2026, 7, 24);
const millisecondesEntreLesDeux = date2.getTime() - date1.getTime();
const joursEntreLesDeux = millisecondesEntreLesDeux / (1000 * 60 * 60 * 24);
console.log("[Date] jours entre les deux dates (calcul manuel) =", joursEntreLesDeux);
const p1 = Temporal.PlainDate.from({ year: 2026, month: 1, day: 1 });
const p2 = Temporal.PlainDate.from({ year: 2026, month: 8, day: 24 });
console.log("[Temporal] durée entre les deux dates =", p1.until(p2).toString());
// ------------------------------------------------------------------------
// en résumé : Temporal est immuable, sans ambiguïté de fuseau horaire, avec
// des types séparés pour chaque usage (date seule, heure seule, instant précis,
// date+heure+fuseau...), et une arithmétique fiable même autour des changements
// d'heure d'été et des mois de longueurs différentes.
// ------------------------------------------------------------------------
|
- [piège n°1] : les mois de Date sont numérotés de 0 à 11 (new Date(2026, 7, 24) désigne pourtant bien août) — source classique de confusion pour un débutant ;
- [piège n°2] : Date est mutable — modifier un objet Date reçu en paramètre d'une fonction modifie aussi l'original, ce qui peut surprendre ; Temporal.PlainDate.add() rend toujours un nouvel objet ;
- [piège n°3] : l'analyse d'une chaîne par new Date(...) est ambiguë selon son format exact (heure locale ou UTC) ; Temporal est toujours explicite ;
- [piège n°4] : Date ne sait pas nativement calculer une durée « propre » entre deux dates (il faut soustraire des timestamps en millisecondes et diviser à la main) ; Temporal.PlainDate.until(...) le fait directement.