Leçons 1/4 · 15 min
TypeScript et montants : ne pas perdre un chiffre
Un formulaire de transfert qui ment
Un développeur écrit un formulaire de transfert. L'utilisateur tape un montant, le code le convertit en nombre, l'envoie. Tout marche en test avec des petits montants. Pourtant, en JavaScript, 0.1 + 0.2 donne 0.30000000000000004, et un grand montant peut être arrondi sans aucun message. Quand c'est de l'argent, une erreur silencieuse est la pire.
TypeScript est JavaScript auquel on ajoute des types : des étiquettes (nombre, texte, liste…) vérifiées par un compilateur avant l'exécution. tsc --noEmit lance cette vérification seule. Les types disparaissent à l'exécution : ils attrapent des bogues pendant que vous écrivez, mais ne protègent pas contre des données mal formées qui arrivent du réseau. Vite est l'outil qui sert la page en développement et la regroupe pour la production ; il transforme le TypeScript en JavaScript sans le vérifier, donc la vérification des types reste une étape à lancer vous-même.
Le piège des nombres
Un nombre JavaScript ordinaire est un nombre à virgule flottante sur 64 bits. Les entiers n'y sont exacts que jusqu'à 2^53 − 1 = 9 007 199 254 740 991 (Number.MAX_SAFE_INTEGER). Or beaucoup de chaînes comptent en très petites unités : un jeton à 18 décimales vaut 10^18 unités, bien au-delà.
Exemple. Un solde de 1 000 000 000 000 000 001 unités (un jeton plus une unité) :
console.log(Number(1000000000000000001n)); // 1000000000000000000
console.log(1000000000000000001n); // 1000000000000000001nAvec Number, l'unité en plus a disparu, sans erreur. Le type `bigint` (entiers de taille arbitraire, suffixe n) garde tout. Règles pratiques :
- Représenter un montant en plus petite unité par un
bigint, jamais par unnumberdécimal. - Mélanger
bigintetnumberdans une opération lève une erreur (TypeError), ce qui est un bon signal. JSON.stringifyéchoue sur unbigint: convertissez en texte avectoString()pour l'envoyer ou le stocker.- Ne convertissez en texte décimal « lisible » qu'au dernier moment, à l'affichage.
Convertir ce que l'utilisateur tape
On évite le calcul à virgule : on découpe la chaîne.
function toUnits(text: string, decimals: number): bigint {
if (!/^\d+(\.\d*)?$/.test(text)) throw new Error("Montant invalide");
const [whole, frac = ""] = text.split(".");
if (frac.length > decimals) throw new Error("Trop de décimales");
return BigInt(whole + frac.padEnd(decimals, "0"));
}
// toUnits("1.5", 18) -> 1500000000000000000n
// toUnits("1.5.5", 18) -> erreur (validé en entier avant tout découpage)On valide d'abord toute la chaîne avec une expression régulière : sans cela, un découpage qui ne garde que les deux premiers morceaux accepterait « 1.5.5 » et le lirait silencieusement comme 1.5. Le cas « trop de décimales » est refusé plutôt qu'arrondi : mieux vaut un message qu'un montant différent de celui que l'utilisateur croit envoyer. (Les littéraux bigint comme 1n demandent une cible de compilation ES2020 ou plus, ce qui est le cas des modèles Vite récents.)
Des types qui décrivent les états
Les unions de types rendent impossible de combiner n'importe quoi :
type Wallet =
| { kind: "disconnected" }
| { kind: "connected"; account: string; chainId: number };Dans l'état disconnected, il n'y a pas de champ account : le compilateur refuse de le lire. C'est la base des deux leçons suivantes. Activez aussi le mode strict ("strict": true dans tsconfig.json) : sans lui, beaucoup de ces garanties s'évaporent.
Ce que cela ne garantit PAS
- Les types sont une promesse faite au compilateur. Une réponse JSON reçue du nœud est de type inconnu tant que vous ne l'avez pas vérifiée : écrire
as Balancefait taire le compilateur, pas le problème. - Un montant exact n'est pas un montant raisonnable : validez aussi le plafond, le solde disponible et l'adresse.
- La validation du formulaire ne protège rien côté serveur ou chaîne : la chaîne refusera ou acceptera de toute façon.
À retenir : montants en bigint et en plus petite unité, texte découpé plutôt que calculé, unions de types pour les états.
