La plupart des langages distinguent les entiers des décimaux. JavaScript n'en a qu'un seul, ce qui simplifie beaucoup de choses et en complique une : la précision des calculs sur des valeurs à virgule.
C'est l'origine du fameux 0.1 + 0.2 qui ne donne pas 0.3. Ce n'est ni un bogue ni une particularité du langage, et savoir d'où cela vient évite de perdre une soirée sur un total de panier.
Définition
Number est le type qui représente les nombres en JavaScript, entiers comme décimaux, sous la forme d'un flottant sur 64 bits. Il couvre les entiers exactement jusqu'à environ neuf millions de milliards, valeur exposée par Number.MAX_SAFE_INTEGER.
console.log(typeof 49, typeof 4.9); // "number" "number"
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false
console.log((0.1 + 0.2).toFixed(2)); // "0.30", une chaîne
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991L'écart vient de la représentation binaire : certains décimaux, dont un dixième, n'ont pas d'écriture exacte en base 2, exactement comme un tiers n'en a pas en base 10. L'erreur est minuscule mais réelle, et elle s'accumule.
Pour de l'argent, la pratique la plus sûre est de compter en centimes, donc en entiers, et de ne diviser qu'au moment de l'affichage. Un total calculé en euros décimaux finit toujours par produire un centime fantôme.
Convertir et vérifier
Une saisie utilisateur arrive toujours sous forme de String (chaîne de caractères). La convertir est la première chose à faire, et vérifier le résultat la seconde.
| Appel | Résultat |
|---|---|
Number("49") | 49 |
Number("49 €") | NaN |
parseInt("49 €", 10) | 49 |
Number("") | 0 |
Number(null) | 0 |
Les deux dernières lignes sont des pièges classiques : une valeur absente devient zéro sans le moindre avertissement. C'est un effet direct de la Coercition (conversion de type), et la seule protection est de tester la présence avant de convertir.
const saisie = "49 €";
const valeur = Number(saisie);
if (Number.isNaN(valeur)) {
console.log("saisie invalide");
} else {
console.log(valeur * 2);
}Deux valeurs particulières
NaNsignale un calcul impossible. Il n'est égal à rien, pas même à lui-même, donc seulNumber.isNaN()le détecte de façon fiable.Infinityapparaît sur une division par zéro. Le langage ne lève aucune erreur, il renvoie cette valeur, qui contamine ensuite tous les calculs suivants.
Questions fréquentes
Comment afficher un prix correctement ?
Avec Intl.NumberFormat, qui gère la devise, le séparateur décimal et l'espace insécable selon la langue. new Intl.NumberFormat("fr-FR", { style: "currency", currency: "EUR" }).format(49) renvoie "49,00 €". C'est nettement préférable à un toFixed suivi d'une concaténation, qui produit un format faux dès qu'on change de pays.
Que faire au-delà de MAX_SAFE_INTEGER ?
Le type BigInt existe pour cela : on écrit 9007199254740993n, avec un n final. Il se rencontre surtout sur des identifiants venant d'une base de données, qui doivent alors transiter en texte plutôt qu'en nombre pour ne pas être arrondis en route.
toFixed renvoie-t-il un nombre ?
Non, il renvoie une chaîne, ce qui surprend souvent. Enchaîner un calcul dessus déclenche une concaténation au lieu d'une addition. Si le résultat doit repartir dans un calcul, reconvertissez-le avec Number(), ou mieux, gardez le nombre et n'arrondissez qu'à l'affichage. Voir aussi Math pour les arrondis qui restent numériques.