Pourquoi Date pose autant de problèmes

La gestion des dates en JavaScript a longtemps reposé sur l’objet Date. Il fonctionne, mais il mélange plusieurs responsabilités : instant précis dans le temps, représentation locale, parsing de chaînes, fuseaux horaires, mutation interne et formatage implicite. Résultat : beaucoup de bugs ne viennent pas d’une erreur visible, mais d’une ambiguïté silencieuse.

Un exemple classique :

const date = new Date("2026-03-29");
console.log(date.toString());

Selon le fuseau horaire de l’environnement, l’affichage peut tomber sur le jour attendu ou sur la veille. La chaîne ressemble à une date civile, mais Date la convertit en instant temporel. Pour une réservation, une facture, un anniversaire ou une date limite administrative, ce comportement est rarement celui que l’on veut.

Temporal vise précisément à corriger ce problème : séparer clairement les concepts. Une date civile n’est pas un instant. Une heure locale n’est pas un timestamp. Une durée n’est pas une différence calendaire. Cette séparation rend le code plus verbeux, mais aussi beaucoup plus fiable.

Les grands concepts de Temporal

Temporal introduit plusieurs types spécialisés. Les plus importants à connaître sont :

  • Temporal.PlainDate pour une date sans heure ni fuseau, comme 2026-06-03 ;
  • Temporal.PlainTime pour une heure sans date, comme 14:30:00 ;
  • Temporal.PlainDateTime pour une date et une heure sans fuseau ;
  • Temporal.Instant pour un point exact sur la ligne du temps ;
  • Temporal.ZonedDateTime pour un instant associé à un fuseau horaire ;
  • Temporal.Duration pour représenter une durée structurée.

Cette distinction est essentielle. Dans un formulaire HTML où l’utilisateur choisit une date de début de formation, on veut probablement une PlainDate. Pour enregistrer le moment exact où une commande a été validée, on veut plutôt un Instant. Pour planifier un webinaire à Paris visible depuis plusieurs pays, on a besoin d’un ZonedDateTime.

La documentation de référence est disponible sur MDN, et le projet dispose aussi d’un polyfill officiel sur GitHub.

Installer Temporal aujourd’hui

Selon l’environnement ciblé, Temporal peut nécessiter un polyfill. Dans une application TypeScript moderne, on peut l’ajouter ainsi :

npm install @js-temporal/polyfill

Puis l’importer dans le code applicatif :

import { Temporal } from "@js-temporal/polyfill";

const today = Temporal.Now.plainDateISO();
console.log(today.toString());

Dans une base de code existante, je conseille d’éviter de remplacer toutes les utilisations de Date d’un seul coup. Commencez par les zones sensibles : réservation, facturation, échéances, rappels, synchronisation internationale, logs métier. Comme pour le typage des données API en TypeScript, l’objectif n’est pas d’ajouter de la complexité décorative, mais de rendre les états invalides plus difficiles à produire.

Manipuler une date civile sans fuseau horaire

Prenons un cas simple : une plateforme de formation doit afficher la prochaine session d’un cours. La session a lieu le 15 septembre 2026. Cette information n’est pas un instant universel ; c’est une date civile.

import { Temporal } from "@js-temporal/polyfill";

const sessionDate = Temporal.PlainDate.from("2026-09-15");

const nextWeek = sessionDate.add({ days: 7 });
const previousMonth = sessionDate.subtract({ months: 1 });

console.log(sessionDate.toString());
console.log(nextWeek.toString());
console.log(previousMonth.toString());

Avec Date, ajouter un mois ou un jour peut produire des surprises, notamment autour des changements d’heure. Avec PlainDate, on exprime clairement une opération calendaire. Il n’y a pas de fuseau horaire caché, pas de conversion implicite en millisecondes, pas de mutation de l’objet original.

On peut aussi comparer deux dates :

const start = Temporal.PlainDate.from("2026-09-15");
const end = Temporal.PlainDate.from("2026-09-19");

const duration = start.until(end);

console.log(duration.days); // 4

Cette approche est particulièrement utile pour les interfaces de planning, les calendriers éditoriaux, les exports comptables ou les formulaires d’inscription.

Travailler avec les fuseaux horaires

Un webinaire annoncé à 18 h à Paris ne se résume pas à 18:00. Pour un participant à Montréal ou Tokyo, l’heure affichée doit être convertie. Ici, Temporal.ZonedDateTime est le bon outil.

import { Temporal } from "@js-temporal/polyfill";

const parisEvent = Temporal.ZonedDateTime.from({
  year: 2026,
  month: 10,
  day: 12,
  hour: 18,
  minute: 0,
  timeZone: "Europe/Paris"
});

const montrealEvent = parisEvent.withTimeZone("America/Toronto");

console.log(parisEvent.toString());
console.log(montrealEvent.toString());

Le point important : on ne manipule pas seulement une heure locale, mais une heure locale liée à un fuseau IANA explicite. Cela évite les approximations du type UTC+1, qui ne suffisent pas à gérer les changements d’heure d’été et d’hiver.

Pour l’affichage final, Temporal ne remplace pas forcément Intl. Les deux API sont complémentaires. Temporal structure correctement les données temporelles ; Intl.DateTimeFormat les formate pour l’utilisateur. J’ai déjà abordé cette logique côté nombres, dates et devises dans Intl en JavaScript.

const formatter = new Intl.DateTimeFormat("fr-FR", {
  dateStyle: "full",
  timeStyle: "short",
  timeZone: "Europe/Paris"
});

const instant = parisEvent.toInstant();
const date = new Date(Number(instant.epochMilliseconds));

console.log(formatter.format(date));

Tant que l’écosystème d’affichage repose encore largement sur Date, cette conversion peut rester nécessaire en bordure de l’application. L’important est de ne pas laisser Date redevenir le modèle métier central.

Parser les données reçues d’une API

Supposons qu’une API renvoie une date de début et une date de fin pour une formation :

{
  "title": "Formation TypeScript avancé",
  "startDate": "2026-11-03",
  "endDate": "2026-11-05"
}

Côté TypeScript, on peut normaliser ces chaînes dès l’entrée :

import { Temporal } from "@js-temporal/polyfill";

type ApiTraining = {
  title: string;
  startDate: string;
  endDate: string;
};

type Training = {
  title: string;
  startDate: Temporal.PlainDate;
  endDate: Temporal.PlainDate;
  durationInDays: number;
};

export function parseTraining(input: ApiTraining): Training {
  const startDate = Temporal.PlainDate.from(input.startDate);
  const endDate = Temporal.PlainDate.from(input.endDate);

  return {
    title: input.title,
    startDate,
    endDate,
    durationInDays: startDate.until(endDate).days + 1
  };
}

Cette fonction transforme des chaînes JSON en objets métier explicites. C’est la même logique que pour la validation ou la normalisation d’une réponse API : on évite de propager des données faiblement structurées dans toute l’interface.

Exemple React : afficher une échéance lisible

Dans un composant React, on peut recevoir une date ISO, la convertir en PlainDate, puis calculer un statut simple.

import { Temporal } from "@js-temporal/polyfill";

type DeadlineProps = {
  label: string;
  date: string;
};

export function Deadline({ label, date }: DeadlineProps) {
  const today = Temporal.Now.plainDateISO();
  const deadline = Temporal.PlainDate.from(date);
  const remainingDays = today.until(deadline).days;

  const status =
    remainingDays < 0
      ? "Dépassée"
      : remainingDays === 0
        ? "Aujourd’hui"
        : `Dans ${remainingDays} jour${remainingDays > 1 ? "s" : ""}`;

  return (
    <p>
      <strong>{label}</strong> : {deadline.toString()} — {status}
    </p>
  );
}

Dans une vraie application, on pourra enrichir ce composant avec un formatage localisé via Intl, une gestion d’erreur si la date API est invalide, et des tests unitaires sur les cas limites. Pour les formulaires qui produisent ces dates, les mêmes principes de robustesse rejoignent ceux des formulaires accessibles : l’utilisateur doit comprendre ce qu’il saisit, et le code doit refuser les ambiguïtés.

Bonnes pratiques d’architecture

Temporal donne de meilleurs outils, mais ne remplace pas une architecture claire. Voici les règles que j’applique en formation et en audit de code :

  1. utilisez PlainDate pour les dates civiles, jamais un timestamp ;
  2. utilisez Instant pour les événements techniques horodatés ;
  3. utilisez ZonedDateTime pour les rendez-vous liés à un fuseau ;
  4. convertissez les chaînes API dès la frontière applicative ;
  5. évitez de stocker des objets Temporal directement dans du JSON ;
  6. gardez Date pour les API qui l’exigent, pas comme modèle métier ;
  7. testez les changements d’heure, fins de mois et années bissextiles.

Un test simple peut déjà éviter beaucoup de régressions :

import { Temporal } from "@js-temporal/polyfill";

const start = Temporal.PlainDate.from("2026-01-31");
const nextMonth = start.add({ months: 1 });

console.log(nextMonth.toString()); // 2026-02-28

Ce résultat est explicite : ajouter un mois au 31 janvier produit le dernier jour valide de février. Ce type de comportement doit être connu, testé et documenté dans les règles métier.

Ce qu’il faut retenir

Temporal ne sert pas seulement à écrire une syntaxe plus moderne que Date. Son intérêt principal est conceptuel : nommer correctement les différents types de temps que manipule une application web. Une date d’anniversaire, une heure de rendez-vous, une durée de session, un instant de connexion et une échéance métier ne devraient pas tous être modélisés par le même objet.

Pour une application simple, Date peut encore suffire. Mais dès que le produit touche aux fuseaux horaires, aux calendriers, aux échéances, aux réservations ou à l’internationalisation, Temporal devient un excellent moyen de réduire les bugs implicites. Comme souvent en développement web, la fiabilité ne vient pas d’une abstraction magique, mais d’un modèle plus précis du problème réel.