Sept fonctionnalités natives de JavaScript qui remplacent les dépendances d’outils
How Temporal élimine le code générique courant en JavaScript grâce à l’utilisation de déclarations, d’opérations Map upserts, de la fonction Math.sumPrecise, d’aideurs d’itérateur, de méthodes Set et de Promise.withResolvers.
Autrefois, pour lancer un projet sérieux en JavaScript, il fallait installer une bibliothèque de dates, un outil de collecte et un assistant de regroupement, puis envelopper chaque ressource chargée dans des blocs try...finally de protection. Au cours des derniers cycles de publication, une série de propositions du TC39, débattues pendant des années, est désormais intégrée à la norme et disponible dans les navigateurs éternels, Node.js, Bun et Deno. Ce guide présente sept d’entre elles, le problème qu’elles résolvent et les points à vérifier avant de supprimer une dépendance.
Une remarque sur la compatibilité avant de commencer : ces fonctionnalités sont arrivées dans les moteurs à des moments différents, certaines étant très récentes. Vérifiez les données de compatibilité actuelles pour vos navigateurs et versions de runtime cibles, et utilisez un polyfill ou la bibliothèque ancienne lorsque la compatibilité n’est pas encore garantie.
Temporal : une véritable API pour les dates et les heures
Tout ceux qui ont utilisé l’objet Date intégré pendant plus de quelques minutes ont rencontré ses pièges :
- Les mois sont numérotés à partir de zéro (
0correspond au janvier et11au décembre), tandis que les jours du mois commencent à1. - Les instances de
Datesont modifiables, ce qui signifie que leur transmission à une fonction d’aide peut modifier silencieusement la valeur de l’appelant. - La conversion des fuseaux horaires ainsi que les calculs liés à l’heure d’été exigent des opérations manuelles complexes ou l’utilisation de bibliothèques telles que date-fns, Day.js ou Luxon.
Temporal est une nouvelle API immuable pour la gestion des dates et des heures conçue pour remplacer Date. Elle ne supprime pas Date du langage, car le code existant continue de fonctionner, mais le nouveau code n’a plus besoin de lui.
Types distincts pour des concepts distincts
La principale décision de conception de Temporal consiste à modéliser différentes idées sous forme de types différents :
Temporal.PlainDate: une date calendrier sans heure ni fuseau horaire, comme une date d’anniversaire.Temporal.PlainTime: une heure indiquée sur un cadran solaire sans date, comme une alarme à 07:00.Temporal.ZonedDateTime: un moment précis lié à un fuseau horaire et à un calendrier spécifiques.
Le fragment ci-dessous lit l’heure actuelle à New York, ajoute 14 jours à une date simple, et mesure l’écart entre deux dates en mois et en jours. Notez que la différence est demandée avec largestUnit: 'month', ce qui fait que le résultat s’affiche en mois plus jours plutôt qu’en simple nombre de jours.
// 1. Getting current time in a specific time zone
const meetingTime = Temporal.Now.zonedDateTimeISO('America/New_York');
console.log(meetingTime.toString());
// e.g. "2026-09-04T12:09:28-04:00[America/New_York]"
// 2. Safe, readable date math
const deadline = Temporal.PlainDate.from('2026-09-01');
const followUp = deadline.add({ days: 14 });
console.log(followUp.toString()); // "2026-09-15"
// 3. Calculating the exact difference between two dates
const start = Temporal.PlainDate.from('2026-01-01');
const end = Temporal.PlainDate.from('2026-09-04');
const diff = start.until(end, { largestUnit: 'month' });
console.log(`${diff.months} months and ${diff.days} days`);
// "8 months and 3 days"
Tout objet Temporal est immuable. Appeler add() ne modifie jamais l’objet original ; il renvoie un nouvel objet. Pour de nombreux projets, cela signifie qu’une bibliothèque de dates peut être exclue du bundle. Avant de migrer, consultez les pièges courants liés à Temporal.
Gestion explicite des ressources avec using
Une source fréquente de fuites et de verrous bloqués est l’oubli de fermer ce que l’on a ouvert :
- un handle de fichier resté ouvert après lecture
- une connexion à la base de données restée active lorsque une route API génère une erreur
- des écouteurs d’événements ou des threads de travail qui ne sont jamais détachés
La solution traditionnelle est try...finally. Lorsqu’il s’agit de trois ressources dans une seule fonction, cela se transforme rapidement en du code générique fortement imbriqué. La gestion explicite des ressources fait appel à la déclaration using ainsi qu’à deux symboles bien connus : Symbol.dispose et Symbol.asyncDispose.
Comment la suppression des ressources est déclenchée
Lorsqu’une valeur est déclarée avec using, le moteur appelle sa méthode [Symbol.dispose]() dès que l’exécution quitte le bloc contenant cette déclaration, que ce soit de manière normale ou en raison d’une erreur. Si plusieurs ressources sont déclarées dans un même contexte, elles sont supprimées dans l’ordre inverse de leur déclaration, ce qui correspond exactement à ce que feraient des blocs try...finally imbriqués.
Dans l’exemple, DatabaseSession met en œuvre le mécanisme de suppression, et runReport déclare une session à l’aide de using. L’ordre des enregistrements montre que le nettoyage s’exécute après la requête, lorsque la fonction se termine :
// Define a resource that knows how to clean itself up
class DatabaseSession {
constructor(dbName) {
this.connection = `Connected to ${dbName}`;
console.log("Database opened.");
}
query(sql) {
return `Results for: ${sql}`;
}
// Built-in disposal hook
[Symbol.dispose]() {
console.log("Database connection closed automatically!");
}
}
// Using the resource
function runReport() {
using session = new DatabaseSession("AnalyticsDB");
const data = session.query("SELECT * FROM metrics");
console.log(data);
// When runReport finishes (or if it throws),
// session[Symbol.dispose]() runs immediately.
}
runReport();
// Logs:
// 1. "Database opened."
// 2. "Results for: SELECT * FROM metrics"
// 3. "Database connection closed automatically!"
Pour un nettoyage asynchrone, comme la fermeture d’un socket réseau, il faut implémenter [Symbol.asyncDispose]() et déclarer la ressource à l’aide de await using à l’intérieur d’une fonction asynchrone. Le moteur d’exécution attend alors le nettoyage avant de continuer.
Mises à jour ou créations dans un Map avec getOrInsert et getOrInsertComputed
Map est la structure appropriée pour les collections clé-valeur, mais le schéma « lire ou créer si absent » a toujours été peu pratique. Le regroupement des valeurs sous une clé se fait généralement de cette manière :
// The old way: multiple lookups and manual branching
if (!userCache.has(userId)) {
userCache.set(userId, fetchDefaultProfile(userId));
}
const profile = userCache.get(userId);
Cela correspond à deux recherches (has suivi de get) ainsi qu’à une branche, simplement pour s’assurer qu’une entrée existe. Les nouvelles méthodes regroupent tout cela en une seule appel et une seule recherche :
map.getOrInsert(key, defaultValue): retourne la valeur existante pourkey, ou stockedefaultValuesous cette clé avant de la renvoyer.map.getOrInsertComputed(key, callback): fonctionne de la même manière, mais le callback n’est exécuté que lorsque la clé est absente, évitant ainsi la création inutile d’une valeur par défaut coûteuse.
Cette distinction est importante pour l’exemple ci-dessus. getOrInsert(userId, fetchDefaultProfile(userId)) appellerait fetchDefaultProfile à chaque accès, car les arguments de la fonction sont évalués avant l’appel ; la version calculée évite cela. Le fragment suivant regroupe les événements par identifiant d’utilisateur, ne créant un tableau vide que la première fois qu’un utilisateur apparaît :
const userActivity = new Map();
// Grouping events under user IDs
function logEvent(userId, eventName) {
// If userId doesn't exist, create an empty array, insert it, and return it.
const events = userActivity.getOrInsertComputed(userId, () => []);
events.push(eventName);
}
logEvent("user_42", "login");
logEvent("user_42", "clicked_button");
console.log(userActivity.get("user_42"));
// ['login', 'clicked_button']
Math.sumPrecise pour des totaux précis
Les nombres JavaScript suivent le standard IEEE 754 à double précision en virgule flottante, ce qui produit le résultat célèbre ci-dessous :
0.1 + 0.2; // 0.30000000000000004
Dans les factures, les paniers ou les agrégations de graphiques, de tels erreurs s’accumulent jusqu’à ce que le total soit visiblement incorrect.
Math.sumPrecise() prend un ensemble d’entiers numériques et calcule leur somme comme s’il y avait une précision illimitée, en arrondissant uniquement une fois à la fin. Une utilisation naïve de reduce entraîne un arrondi à chaque étape, ce qui provoque l’écart de résultat. Dans l’exemple, les quatre éléments des factures donnent une somme de 0.9999999999999999 avec reduce, tandis que Math.sumPrecise renvoie 1 (la console affiche 1, et non 1.0) :
const invoiceItems = [0.1, 0.2, 0.3, 0.4];
// Old reduce approach:
const naiveTotal = invoiceItems.reduce((acc, n) => acc + n, 0);
console.log(naiveTotal);
// 0.9999999999999999
// New Math.sumPrecise approach:
const accurateTotal = Math.sumPrecise(invoiceItems);
console.log(accurateTotal);
// 1.0
Il est important de comprendre cette limite. Math.sumPrecise élimine l’erreur d’arrondi accumulée, mais il ne peut pas changer le fait que des valeurs comme 0.1 ne sont pas exactement représentables en binaire. La somme de [0.1, 0.2] donne toujours 0.30000000000000004, car la somme exacte de ces deux nombres doubles s’arrondit à cette valeur. Cela représente une véritable amélioration pour les statistiques et les tableaux de bord ; en revanche, pour les montants financiers, des unités mineures entières (centimes) ou une bibliothèque décimale restent le choix le plus sûr.
Aides pour les itérateurs et Iterator.concat
Pendant plus de dix ans, les tableaux ont offert .map(), .filter() et .slice(), tandis que les itérateurs et générateurs, tels que les séquences infinies ou les curseurs de base de données, ne disposaient d’aucun équivalent. Pour utiliser .map() sur un générateur produisant des milliers de lignes, il fallait d’abord convertir l’ensemble en tableau à l’aide de [...generator()], ce qui annulait les avantages en termes de mémoire liés au streaming.
Les outils d’aide pour les itérateurs ajoutent directement des méthodes telles que map, filter, take, drop et flatMap aux itérateurs, et Iterator.concat() combine plusieurs itérateurs en un seul. Tous sont de type « lazy » : un élément n’est calculé que lorsque l’utilisateur en fait la demande.
L’exemple relie des outils aux un générateur infini. Comme l’évaluation est différée et que take(3) s’arrête après trois valeurs, la boucle se termine même si la source ne finit jamais :
function* infiniteCounter() {
let count = 1;
while (true) {
yield count++;
}
}
// Grab an iterator from our infinite generator
const stream = infiniteCounter()
.filter(num => num % 2 === 0) // Keep even numbers
.map(num => `Count: ${num}`) // Format them
.take(3); // Stop after 3 values
for (const item of stream) {
console.log(item);
}
// Logs:
// "Count: 2"
// "Count: 4"
// "Count: 6"
Pour assembler plusieurs séquences sans créer un tableau intermédiaire, passez-les à Iterator.concat(). Celui-ci consomme chaque itérateur à son tour :
const firstBatch = [1, 2, 3].values();
const secondBatch = [4, 5, 6].values();
const combined = Iterator.concat(firstBatch, secondBatch);
console.log([...combined]); // [1, 2, 3, 4, 5, 6]
Les outils d’itération eux-mêmes sont disponibles depuis longtemps dans les principaux moteurs ; Iterator.concat() est plus récent, il faut donc l’examiner séparément.
Opérations natives sur les ensembles
Pendant longtemps, un Set ne proposait que .has(), .add() et .delete(). Tout ce qui ressemblait à une intersection nécessitait de convertir en tableaux et de filtrer manuellement :
// The old manual way:
const intersection = new Set([...setA].filter(x => setB.has(x)));
Set.prototype intègre désormais une véritable algèbre des ensembles : union, intersection, difference, symmetricDifference, ainsi que les prédicats isSubsetOf, isSupersetOf et isDisjointFrom. Chaque opération renvoie un nouveau Set sans modifier les ensembles originaux.
L’exemple compare deux ensembles de rôles, extrait les permissions réservées uniquement aux administrateurs et vérifie que les rôles de l’éditeur font partie du sous-ensemble de ceux des administrateurs :
const adminRoles = new Set(['read', 'write', 'delete', 'audit']);
const editorRoles = new Set(['read', 'write']);
// Find privileges exclusive to admins
const adminOnly = adminRoles.difference(editorRoles);
console.log([...adminOnly]); // ['delete', 'audit']
// Check role containment
console.log(editorRoles.isSubsetOf(adminRoles)); // true
Ils évitent également les tableaux temporaires créés par l’approche basée sur le déploiement et le filtrage.
Promise.withResolvers pour les promesses différées
Parfois, une promesse doit être résolue en dehors de son constructeur, par exemple via un gestionnaire d’événement ou une fonction de rappel enregistrée ailleurs. Ce schéma « différé » nécessitait auparavant que des variables soient extraites du bloc d’exécution :
// The clunky way
let resolveFn, rejectFn;
const myPromise = new Promise((res, rej) => {
resolveFn = res;
rejectFn = rej;
});
Promise.withResolvers() renvoie la promesse ainsi que ses fonctions resolve et reject dans un seul objet. Dans l’exemple, un gestionnaire de clic résout la promesse tandis que d’autres codes attendent simplement son résultat. Notez que ce fragment utilise await au niveau le plus élevé, il doit donc être exécuté dans un module ES ou à l’intérieur d’une fonction async :
const { promise, resolve, reject } = Promise.withResolvers();
// Attach your listeners or pass the resolve handle to an event handler
document.getElementById("submit-btn").addEventListener("click", () => {
resolve("User clicked submit!");
}, { once: true });
// Await the promise wherever you need it
const message = await promise;
console.log(message);
Ce schéma est utile pour des files d’attente d’événements personnalisées, des ponts vers des Web Workers, ainsi que pour adapter des API basées sur des fonctions de rappel ou des émetteurs à du code async.
Quelles en sont les conséquences pour vos dépendances
Le fil conducteur est la parité : ce langage couvre désormais des tâches courantes telles que les dates, le nettoyage des ressources, le regroupement par clé et les opérations mathématiques sur des ensembles, qui étaient auparavant confiées aux bibliothèques utilisateur. Avant d’ajouter un autre paquet lors de votre prochain sprint, vérifiez vos objectifs :
- Si vous visez les navigateurs modernes ou les versions actuelles de Node.js, Bun ou Deno, de nombreux outils correspondants existent déjà globalement.
- Chaque dépendance que vous évitez signifie un bundle plus petit, un démarrage plus rapide et du code que tout développeur JavaScript peut lire sans avoir à apprendre une API tierce.
Points clés
- Temporal propose des types immuables et clairement séparés pour les dates ; il remplace
Datedans le nouveau code sans pour autant le supprimer. usingetawait usingrendent le nettoyage automatique et ordonné, en remplaçant les blocstry...finallyimbriqués.
getOrInsertComputed lorsque la valeur par défaut est coûteuse à créer.Math.sumPrecise corrige l’écart d’arrondi accumulé, pas la représentation binaire ; conservez les montants sous forme d’entiers.Lectures complémentaires
- Six fonctionnalités natives HTML qui remplacent les librairies UI JavaScript courantes — Découvrez comment popover, exclusive details, dialog, Declarative Shadow DOM, fetchpriority et datalist remplacent le JavaScript personnalisé, ainsi que les limites de chacune d’elles.
- Les fonctionnalités de Node.js 26 qui remplacent discrètement des années de solutions de contournement — Découvrez l’API Temporal de Node.js 26, l’exécution native de TypeScript, les outils d’optimisation du cache et autres ajouts qui éliminent des solutions de contournement utilisées depuis longtemps.
- Auditing utils.js : Six fonctions intégrées qui remplacent des outils personnalisés — Identifiez quelles API JavaScript natives peuvent remplacer les méthodes de clonage profond, les copies de sécurité, les délais manuels, les promesses différées ainsi que lodash groupBy, et quels sont les points à considérer.