JavaScript moderne : de solide à expert
Texte· 17 min

Les types primitifs et le piège de typeof

Un test de panier qui échoue pour 0,00000000000000004

Un test unitaire sur un total de panier qui compare 0.1 + 0.2 === 0.3 échoue. Pas parfois : toujours. Tape-le dans n'importe quelle console :

0.1 + 0.2            // 0.30000000000000004
0.1 + 0.2 === 0.3    // false

Ce n'est pas un bug de JavaScript. number est un flottant IEEE 754 sur 64 bits, et 0.1 n'a pas de représentation binaire exacte, exactement comme 1/3 n'a pas d'écriture décimale finie. Python et Java font pareil avec leurs doubles. Ce qui est propre à JavaScript, c'est qu'il n'a qu'un seul type numérique flottant pour tout : pas d'entier 32 bits séparé, pas de décimal. D'où l'intérêt de connaître précisément ses types, leurs limites, et l'outil d'inspection typeof avec ses mensonges.

Sept primitifs, et tout le reste est objet

La spec (§6.1) définit sept types primitifs : string, number, bigint, boolean, undefined, symbol, null. Tout le reste est un objet, y compris les tableaux, les fonctions, les dates et les regex. Un primitif est immuable : "abc".toUpperCase() ne modifie pas la chaîne, il en crée une nouvelle.

Détail que peu de gens savent expliquer : si "abc" est un primitif sans propriétés, pourquoi "abc".length marche ? Parce que le moteur crée à la volée un objet wrapper String temporaire (l'opération ToObject de la spec), lit la propriété dessus, puis le jette. C'est l'auto-boxing. Conséquence mesurable : écrire une propriété sur un primitif ne sert à rien, et en mode strict (donc dans tout module ES) ça lève :

"use strict";
const s = "abc";
s.foo = 1; // TypeError: Cannot create property 'foo' on string 'abc'

typeof : sept réponses justes, deux mensonges

typeof "bonjour"    // "string"
typeof 42           // "number"
typeof 42n          // "bigint"
typeof true         // "boolean"
typeof undefined    // "undefined"
typeof Symbol()     // "symbol"
typeof function(){} // "function"
typeof null         // "object"  <-- le bug historique
typeof []           // "object"  <-- vrai mais inutile

typeof null === "object" est un bug de la première implémentation de 1995 : les valeurs étaient taguées par leurs bits de poids faible, null était un pointeur nul, tag 0, donc « object ». Une correction a été proposée puis rejetée par TC39 parce qu'elle cassait trop de sites. Le comportement est aujourd'hui gravé dans la table de la spec (§13.5.3). Pour tester null, compare directement : x === null. Pour un tableau : Array.isArray(v), qui traverse même les frontières de realm (iframe), contrairement à v instanceof Array.

Curiosité de spécialiste, utile en entretien : dans un navigateur, typeof document.all renvoie "undefined" alors que l'objet existe. C'est la seule valeur du web avec le slot interne [[IsHTMLDDA]], une violation volontaire de la spec ECMAScript inscrite dans la spec HTML pour ne pas casser les détections de vieux Internet Explorer.

Les limites réelles de number

Un double 64 bits représente exactement les entiers jusqu'à Number.MAX_SAFE_INTEGER, soit 9 007 199 254 740 991 (2^53 − 1). Au-delà, les entiers se confondent :

2 ** 53 === 2 ** 53 + 1  // true (!)

C'est le genre de piège qui explose quand un identifiant de base de données (un id Twitter/X, par exemple) dépasse 2^53 et arrive en JSON : deux ids distincts deviennent le même number. La parade moderne, c'est bigint : 42n, précision entière arbitraire. Mais on ne mélange pas les deux types :

1n + 1
// TypeError: Cannot mix BigInt and other types, use explicit conversions
1n + BigInt(1)  // 2n

Pour comparer des flottants, oublie l'égalité stricte et compare à un epsilon : Math.abs(a - b) < Number.EPSILON (EPSILON vaut environ 2.22e-16). Pour de l'argent, travaille en centimes entiers, je te déconseille les flottants pour tout ce qui finit sur une facture.

NaN, le number qui ne s'égale pas lui-même

typeof NaN renvoie "number", et NaN === NaN vaut false. C'est la seule valeur du langage non égale à elle-même, un héritage direct d'IEEE 754. Détection fiable : Number.isNaN(v). Jamais le vieux isNaN() global, qui convertit d'abord son argument :

Number.isNaN(NaN)    // true
Number.isNaN("abc")  // false : c'est une chaîne, pas NaN
isNaN("abc")         // true : "abc" est converti en NaN d'abord, trompeur

Dans la même famille de bizarreries : il existe deux zéros, 0 et -0. 0 === -0 vaut true, mais 1 / -0 vaut -Infinity. La fonction Object.is applique un troisième algorithme d'égalité (SameValue) qui distingue les deux cas que === traite mal :

Object.is(NaN, NaN)  // true
Object.is(0, -0)     // false

On recroisera ces algorithmes à la leçon suivante, parce que includes, Set et Map en utilisent un quatrième.

symbol, en deux mots

Un Symbol() est une clé de propriété garantie unique : Symbol("id") === Symbol("id") vaut false, la description entre parenthèses n'est qu'une étiquette de debug. On s'en sert pour poser des métadonnées sur un objet sans risquer de collision avec ses clés, et le langage lui-même expose ses points d'extension ainsi (Symbol.iterator, Symbol.hasInstance). Sache que ça existe et que typeof le reconnaît, on le recroisera avec les itérateurs.

À toi

Sans exécuter : que renvoie typeof typeof 42 ?

"string". typeof 42 s'évalue d'abord et produit la chaîne "number". Puis typeof "number" s'applique à cette chaîne : "string". L'opérateur renvoie toujours une chaîne, donc typeof typeof x vaut "string" pour absolument n'importe quel x, même non déclaré.

Trois choses à garder : typeof ment sur null et ne distingue pas les tableaux ; les entiers ne sont sûrs que jusqu'à 2^53 − 1, au-delà c'est bigint ; Number.isNaN et Object.is couvrent les cas où === trahit. Référence : typeof sur MDN.

On mesure l'audience du site (GA4) et, si vous dites oui, on affine aussi la pub Meta. Rien ne se déclenche avant votre réponse — et votre choix tient 6 mois.