Un problème bien connu des développeurs
Le langage TypeScript, pourtant conçu pour apporter une couche de typage statique à JavaScript, est au cœur d'une critique récurrente : son type number ne transporte aucune information sur l'unité ou la sémantique de la valeur. Un développeur, Daniel Brain, a détaillé dans une analyse récente les limites de cette approche et les correctifs parfois lourds que son équipe a dû mettre en place.
« 5000, c'est 5000, c'est 5000 », résume-t-il, soulignant que TypeScript ne distingue pas une durée en millisecondes d'un prix en centimes ou d'une hauteur en pixels. « Ce n'est pas de la sécurité de typage. C'est une ambiance », écrit-il.
Les bugs liés à cette absence de distinction sont fréquents : un paramètre setTimeout interprété en millisecondes alors qu'il aurait dû l'être en secondes, un calcul de date d'expiration utilisant un TTL en secondes mêlé à un timestamp en millisecondes, ou encore une fonction de facturation qui attend des dollars et reçoit des centimes. « Chaque bug de la catégorie 'je viens de perdre trois heures' a la même forme », constate-t-il.
Une infrastructure maison pour rétablir la rigueur
Face à ce constat, l'équipe de Daniel Brain a développé une série d'outils pour imposer une distinction nette entre les différentes unités. La première brique est l'utilisation du type générique Tagged<T, Name>, une intersection de types qui permet d'associer un nom sémantique à un nombre sans impact au moment de l'exécution. Ainsi, Milliseconds et Seconds deviennent des types distincts aux yeux du compilateur.
« Jusque-là, tout va bien, explique-t-il. Le problème commence dès que l'on tente de faire des opérations arithmétiques. » En effet, l'opérateur + en TypeScript agit sur le type number sous-jacent et dissout les étiquettes. L'addition d'une valeur de type Milliseconds et d'une valeur de type Seconds compile sans erreur et produit un number sans unité, rendant toute détection impossible.
Pour contourner cette limitation, l'équipe a dû écrire une bibliothèque de fonctions mathématiques génériques — add, subtract, multiply, etc. — qui préservent les types étiquetés grâce à un as T interne. Chaque appel arithmétique passe donc par une fonction plutôt que par un opérateur natif. « Dans n'importe quel autre langage typé, on écrirait start + period * index. Ici, on écrit add(start, multiply(period, index)) », déplore Daniel Brain.
Deux règles ESLint sur mesure
Reste à s'assurer que les développeurs n'utilisent pas par inadvertance un opérateur arithmétique sur ces types spécialisés. Une première règle ESLint personnalisée, baptisée no-arithmetic-on-branded-primitives, a donc été créée pour interdire tout +, -, *, / ou % entre deux opérandes de type opaque.
Une seconde règle, no-branded-primitive-cast, bloque les conversions avec as lorsque la valeur provient d'une expression calculée. Seules les conversions de littéraux sont autorisées, car elles constituent souvent le seul moyen d'attribuer un type opaque à une constante.
« Ce dispositif fonctionne, assure le développeur. Il a détecté de vrais bugs lors des revues de code. » Les refontes qui modifient la sémantique d'une unité deviennent « bruyantes et évidentes ».
Un effort collectif en question
Daniel Brain dresse le bilan de l'infrastructure nécessaire pour résoudre un problème simple : « une milliseconde n'est pas une seconde ». Au total, son équipe a développé un type utilitaire Tagged<>, une bibliothèque de fonctions arithmétiques, deux règles ESLint sur mesure, et une convention d'équipe exigeant le passage par des fonctions de conversion pour chaque unité.
« Multipliez cet effort par chaque équipe TypeScript qui se soucie de la correction de son code », écrit-il. « Nous reconstruisons collectivement la même solution en parallèle, dans des milliers de bases de code, parce que le langage ne le fait pas. »
Cette critique rejoint des débats plus larges sur l'évolution de TypeScript : alors que certains appellent à des types numériques unitaires natifs, d'autres préfèrent s'en remettre à des bibliothèques externes. En attendant, chaque développeur confronté à la question des unités doit choisir entre la rigueur imposée par des couches logicielles additionnelles et le risque d'erreurs silencieuses en production.