You Don't Know JS Yet: Get Started
Chapitre 6 / 7 · 10 min de lecture

Les types & la coercition

Troisième pilier : les types primitifs, la conversion implicite (coercition), et l'art d'utiliser == et === à bon escient.

Une phrase revient sans cesse dès qu'on apprend JavaScript : « en JS, tout est un objet ». C'est faux. C'est l'une de ces idées reçues que Kyle Simpson s'attache à corriger, parce qu'elle empêche de comprendre comment le langage manipule réellement vos données. JavaScript possède de vrais types primitifs (primitive types), distincts des objets, et la façon dont il les copie, les passe et les compare obéit à des règles précises — pas à de la magie.

Ce troisième pilier du langage rassemble les sujets les plus mal aimés et les plus caricaturés de JS : typeof et ses bizarreries, la différence entre valeurs et références, et surtout la coercition (coercion), cette conversion implicite de type que la communauté condamne en bloc. La thèse de Simpson est à contre-courant : la coercition n'est pas un défaut à fuir, c'est une fonctionnalité puissante du langage qu'il faut comprendre pour s'en servir à bon escient. Démystifions tout cela.

Des valeurs, pas que des objets

L'unité d'information la plus fondamentale d'un programme est la valeur (value) : ce sont les données, l'état du programme. En JS, les valeurs se rangent en deux catégories — les primitives et les objets.

Les primitives sont les briques de base, et elles sont plus nombreuses qu'on ne le croit :

typeof 42;                  // "number"
typeof "abc";               // "string"
typeof true;                // "boolean"
typeof undefined;           // "undefined"
typeof 9007199254740993n;   // "bigint"
typeof Symbol("clé");       // "symbol"

Au total, sept types primitifs : chaîne (string), nombre (number), booléen (boolean), null, undefined, symbole (symbol) et grand entier (bigint). Aucun n'est un objet. Les chaînes sont des collections ordonnées de caractères ; les nombres servent à compter et à indexer (les tableaux sont indexés à partir de 0) ; le bigint stocke des entiers arbitrairement grands.

Deux primitives méritent une note. null et undefined indiquent toutes deux l'absence de valeur. Beaucoup de développeurs les traitent comme interchangeables, ce qui est possible avec un peu de soin — mais Simpson recommande de n'utiliser que undefined comme valeur « vide » unique, même si null est plus court à taper. Quant au symbole (symbol), c'est une valeur cachée et impossible à deviner, utilisée presque exclusivement comme clé spéciale sur des objets, surtout dans le code bas niveau des bibliothèques.

Note

« Tout est un objet » est un mythe. Les primitives ne sont pas des objets. Si écrire "abc".length fonctionne, c'est parce que JS « emballe » temporairement la chaîne dans un objet le temps de l'accès — mais la valeur sous-jacente, elle, reste une primitive pure.

L'autre famille, ce sont les objets (objects) : collections de valeurs accessibles par clé. Les tableaux (arrays) en sont un sous-type, ordonné et indexé numériquement. Et — fait essentiel — les fonctions (functions) sont elles aussi un sous-type d'objet. Une fonction est une valeur, qu'on peut assigner, stocker dans un tableau ou passer en argument.

typeof et ses pièges

L'opérateur typeof renvoie le type intégré d'une valeur. C'est l'outil de base pour distinguer les types, mais il traîne deux pièges célèbres qu'il faut connaître par cœur.

typeof null;               // "object" -- oups, un bug !
typeof function hello(){}; // "function"
typeof [1, 2, 3];          // "object" -- pas "array"

Premier piège : typeof null renvoie "object" au lieu du "null" attendu. C'est un bug historique de JS, présent depuis ses tout premiers jours, qui ne sera jamais corrigé pour ne pas casser le web existant. Ne comptez donc pas sur typeof pour détecter null.

Second piège, dans l'autre sens : typeof renvoie le très spécifique "function" pour une fonction, alors qu'une fonction n'est qu'un sous-type d'objet. Mais il ne renvoie pas "array" pour un tableau — un tableau est rangé sous le générique "object". Pour reconnaître un tableau, il faut Array.isArray(...).

Attention

Retenez les deux anomalies : typeof null === "object" (faux ami) et typeof unTableau === "object" (pas "array"), tandis que typeof uneFonction === "function". typeof est utile, mais ne le croyez pas aveuglément sur ces cas.

Valeurs vs références : copie ou partage ?

Voici l'un des points qui surprend le plus les développeurs venant d'autres langages. Dans beaucoup de langages, on choisit de passer une donnée par valeur ou par référence. En JS, ce choix n'existe pas : il est entièrement déterminé par le type de la valeur. Les primitives sont toujours copiées par valeur ; les objets (donc aussi tableaux et fonctions) sont toujours assignés et passés par copie de référence.

Commençons par une primitive :

var monNom = "Kyle";
var tonNom = monNom; // copie de la VALEUR

monNom = "Frank";

console.log(monNom); // "Frank"
console.log(tonNom); // "Kyle" -- intact !

tonNom possède sa propre copie de la chaîne. Réassigner monNom n'a aucun effet sur tonNom : deux variables, deux valeurs indépendantes. C'est ce qu'on attend.

Maintenant le même scénario avec un objet — et là, surprise :

var monAdresse = {
  rue: "123 JS Blvd",
  ville: "Austin",
};
var tonAdresse = monAdresse; // copie de la RÉFÉRENCE

monAdresse.rue = "456 TS Ave"; // je déménage...

console.log(tonAdresse.rue); // "456 TS Ave" -- changé aussi !

Comme la valeur est un objet, tonAdresse ne reçoit pas une copie de l'objet, mais une copie de la référence vers le même objet partagé. Modifier l'un revient à modifier l'autre. Les deux variables pointent vers une unique structure en mémoire.

À retenir

JS choisit copie-par-valeur ou copie-par-référence selon le type, et il n'existe aucun moyen de forcer l'autre comportement, dans un sens comme dans l'autre. Primitives : par valeur. Objets, tableaux, fonctions : par référence. Tout le « partage surprise » des objets découle de cette règle unique.

Cette mécanique explique au passage un comportement de comparaison : === compare les objets par identité de référence, pas par contenu. [1,2,3] === [1,2,3] vaut false, car ce sont deux tableaux distincts en mémoire, même contenu mis à part. JS n'offre aucune comparaison structurelle native ; il faut l'implémenter soi-même.

La coercition : une fonctionnalité, pas un défaut

La coercition (coercion), c'est la conversion d'une valeur d'un type vers sa représentation dans un autre type — par exemple d'une chaîne vers un nombre. Convertir explicitement, comme Number("42"), c'est une coercition explicite. Mais JS pratique aussi la coercition implicite : il convertit pour vous, en coulisses, lorsque le contexte l'exige.

C'est précisément cette conversion implicite qui suscite tant de méfiance. Simpson prend le contre-pied du discours dominant : la coercition est un pilier central du langage, pas une option qu'on pourrait raisonnablement éviter. La fuir aveuglément, c'est se priver d'un outil et, paradoxalement, écrire du code plus fragile. La bonne stratégie n'est pas de l'éviter, mais de l'apprendre.

== et === : la vraie différence

On vous a sûrement appris la règle simpliste : « === vérifie la valeur et le type, == ne vérifie que la valeur ». C'est inexact, et cette imprécision est la source de la mauvaise réputation de ==.

La réalité : toutes les comparaisons d'égalité en JS tiennent compte du type, y compris ==. La vraie distinction est ailleurs. Lorsque les deux types sont identiques, == et === font exactement la même chose, sans aucune différence. La divergence n'apparaît que lorsque les types diffèrent : === interdit toute coercition et renvoie false, tandis que == autorise une coercition pour ramener les deux opérandes au même type, puis compare.

42 === "42"; // false -- types différents, pas de coercition
42 == "42";  // true  -- "42" devient le nombre 42, puis 42 == 42
1 == true;   // true  -- true devient 1, puis 1 == 1

Autrement dit, == n'est pas l'opérateur d'« égalité lâche » qu'on décrit comme insouciant des types. Le bon nom, dit Simpson, serait « égalité coercitive » : il préfère les comparaisons primitives et numériques, et convertit pour y parvenir.

Astuce

La règle de Simpson : utilisez === quand les deux valeurs sont du même type (ou peuvent l'être). Utilisez == justement quand les types peuvent différer et que vous voulez une comparaison plus souple — à condition de savoir ce que vous faites. Savoir que == privilégie les nombres vous évite la plupart des pièges ("" == 0 ou 0 == false valent true).

Un cas pratique et lisible où == brille : tester null ou undefined d'un coup.

var valeur; // undefined

// null == undefined vaut true (et rien d'autre n'y est égal)
if (valeur == null) {
  // s'exécute pour null ET pour undefined
}

null == undefined est true ; c'est même la seule égalité coercitive impliquant null. Écrire valeur == null couvre donc proprement les deux cas d'absence, là où === exigerait deux comparaisons.

=== n'est pas si « strict »

On présente === comme la comparaison « stricte », exacte et infaillible. Pourtant, Simpson le souligne avec malice : === ment dans deux cas particuliers.

NaN === NaN; // false -- NaN n'est égal à RIEN, pas même à lui-même
0 === -0;    // true  -- alors que -0 est une valeur bien distincte

NaN (« not a number ») n'est égal à aucune valeur, pas même à un autre NaN. Et -0 — oui, c'est une valeur réelle, distincte, qu'on peut utiliser intentionnellement — est déclaré égal à 0 par ===. Pour ces deux cas, n'utilisez pas === :

Number.isNaN(NaN);      // true -- le test honnête pour NaN
Object.is(-0, 0);       // false -- distingue vraiment -0 de 0
Object.is(NaN, NaN);    // true -- marche aussi pour NaN

Number.isNaN(...) détecte fidèlement NaN ; Object.is(...) distingue -0 de 0 (et gère NaN aussi). Simpson le surnomme avec humour le « quadruple-égal » ====, la comparaison vraiment-vraiment-stricte. Moralité : === n'est pas la comparaison « exacte » absolue qu'on imagine.

La comparaison conditionnelle coercitive

La coercition ne se cache pas que dans ==. Elle agit chaque fois qu'une condition doit décider. Les if, les ternaires ? :, les tests de while et de for effectuent tous une comparaison implicite — et celle-ci est coercitive.

On croit souvent que if (x) revient à if (x == true). Ce modèle mental est faux. Le vrai mécanisme convertit x en booléen avec Boolean(x), puis évalue :

var x = "hello";

if (x) {
  // s'exécute : Boolean("hello") vaut true
}

if (x == true) {
  // ne s'exécute PAS !
}

Pourquoi ce décalage ? Avec x == true, JS coerce true vers le nombre 1, puis tente "hello" == 1, ce qui convertit "hello" en NaN : le test échoue. Avec if (x) en revanche, JS applique Boolean(x) directement. Le modèle exact est donc : if (Boolean(x) === true). La conversion en booléen précède toujours la décision — impossible d'y échapper.

Cette conversion repose sur deux listes à mémoriser. Les valeurs falsy (fausses) deviennent false ; toutes les autres sont truthy (vraies).

Valeurs falsyTout le reste est truthy
false"hello", "0", " " (chaîne non vide)
0, -042, -1, Infinity
"" (chaîne vide)[] (tableau vide !)
null{} (objet vide !)
undefinedfunction(){}
NaNnew Date()

Les valeurs falsy : false, 0 (et -0), 0n, "", null, undefined, NaN. Tout le reste est truthy — y compris les pièges classiques que sont le tableau vide [], l'objet vide {} et la chaîne "0", qui sont tous truthy. Connaître cette courte liste vous permet de lire correctement n'importe quelle condition.

Piège courant

[], {} et "0" sont truthy. Écrire if (monTableau) ne teste donc pas s'il est vide — un tableau vide passe le test. Pour la vacuité d'un tableau, testez if (monTableau.length > 0). Confondre « truthy » et « non vide » est une source de bugs récurrente.

À retenir

  • « Tout est un objet » est faux : JS a sept types primitifs réels (string, number, boolean, null, undefined, symbol, bigint), distincts des objets, tableaux et fonctions.
  • typeof a deux pièges : typeof null === "object" (bug historique), typeof unTableau === "object" (pas "array"), mais typeof uneFonction === "function".
  • Le type décide de la copie : primitives copiées par valeur (indépendantes), objets/tableaux/fonctions partagés par copie de référence (modifier l'un modifie l'autre). Aucun moyen de forcer l'autre comportement.
  • La coercition est une fonctionnalité, pas un défaut : == autorise la conversion de type quand les types diffèrent ; à types égaux, == et === sont identiques. Utilisez == quand les types peuvent différer et que vous le maîtrisez (ex. x == null).
  • === n'est pas infaillible : NaN === NaN vaut false, 0 === -0 vaut true. Préférez Number.isNaN(...) et Object.is(...) pour ces cas.
  • Les conditions coercent en booléen : if (x) équivaut à if (Boolean(x)). Mémorisez les valeurs falsy (false, 0, 0n, "", null, undefined, NaN) ; tout le reste, y compris [] et {}, est truthy.