Truthy ou falsy : quand est-on dans le vrai en JavaScript ?
Par Delicious Insights • Publié le 10 sept. 2012

Lorsqu'on vient à JavaScript, il est tentant de recourir à de bons vieux motifs de code hérités des autres langages que l'on pratique. Un cas classique est l'abréviation de conditions en se reposant sur les « équivalents booléens » des valeurs. Le danger, c'est que JavaScript a beaucoup plus de valeurs falsy que les autres langages, et utilise des protocoles de conversion implicite parfois surprenants.

Dans cet article, nous allons explorer ensemble quelles valeurs sont falsy, et donc lesquelles sont truthy, mais aussi l'impact que ça a sur de nombreux motifs de code courants, avec les pièges que ça peut poser.

Truthy et falsy

Dans un langage de programmation, on dit qu'une valeur est truthy si, considérée dans un contexte booléen (le plus souvent comme seul composant d'une condition, ou comme opérande d'un opérateur booléen tel que && ou ||), elle équivaut à true.

On dit que le protocole de conversion implicite de son type vers Boolean donne true. En JavaScript, ce protocole spécifique est régi, pour JavaScript « classique » (ECMAScript 3e édition) par la section 9.3 de la spécification (ECMA-262). Par complément, toute valeur dont la conversion ne donne pas true donne nécessairement false.

La spécification indique clairement que les types Undefined et Null équivalent toujours à false. Toute chaîne vide (longueur zéro) également. Côté nombres, les zéros (positif ou négatif) et NaN équivalent eux aussi à false. En revanche, n'importe quel autre objet (dont les tableaux et objets litéraux, même vides) équivaudra à true.

Ces règles semblent assez simples, même s'il peut être surprenant de voir que '' est équivalent à false (mais que new Boolean(false), étant un objet, est équivalent à true, un magnifique WTF). Toutefois, n'arrêtez pas là votre lecture, car dans divers cas « piégeux » les conversions implicites s'enchaînent sur une même valeur et donnent un résultat surprenant ! Nous avons vu les règles de base, mais attendez la suite…

Impact sur les motifs de code usuels

Examinons tour à tour les principaux cas où l'emploi booléen d'une valeur autre se présente dans du code JavaScript usuel.

Boucle numérique « façon C »

Une tentation courante consiste à reproduire les boucles numériques du C. Par exemple :

var count = 10;
while (count--) {
  // votre traitement ici
}

Ou encore :

for (var index = data.length - 1; index--; ) {
  // votre traitement ici
}

Pas de problème particulier sur cette boucle, en tout cas vis-à-vis de son équivalent C.

Le même risque potentiel existe, naturellement : si par mégarde le contenu de la boucle modifie le compteur, et que du coup celui-ci « saute » zéro ou est compensé dans son mouvement normal, cette boucle n'aura pas de fin (en C, elle finirait par un overflow/underflow, mais en JS, qui n'a que des nombres flottants, la fin est si loin qu'en pratique on ne patientera jamais assez longtemps, sans parler des bugs causés par le dépassement de limite).

Le piège est notamment là en JS, qui n'a pas de variables de blocs (sauf à disposer d'un JS garanti suffisamment récent pour pouvoir utiliser l'instruction let) :

// Attention, ce code n'a pas de fin !
for (var count = 10; count--; ) {
  console.log(count);
  for (var count = 0; count < 5; ++count) {
    console.log(count);
  }
}

Ahem…

Tester la présence d'un texte

Attention ici à la notion de « présence ». Si seul un texte absolument vide (longueur zéro) vous ennuie, pas de souci, c'est bien la sémantique d'origine :

// Supposons un champ <input type="text" id="firstName" value="">
if ($('#firstName').val()) {
  // Ce code ne sera pas déclenché
}

Mais si vous voulez un texte non-blank, c'est-à-dire contenant autre chose que du whitespace, il faudra préférer une regex (hein ?!) pour vérifier le contenu du texte :

// Supposons un champ <input type="text" id="firstName" value="   ">
if ($('#firstName').val()) {
  // Ce code SERA déclenché (à tort, a priori)
}

var BLANK_TEXT = /^\s*$/;
if (!BLANK_TEXT.test($('#firstName').val())) {
  // Ce code ne sera pas déclenché
}

// Alternativement :
var PRESENT_TEXT = /\S/;
if (PRESENT_TEXT.test($('#firstName').val())) {
  // Ce code ne sera pas déclenché
}

Tester la présence de nœuds du DOM

Si getElementById renvoie bien null s'il ne trouve pas l'élément d'id indiqué, l'immense majorité d'entre nous utilisons une bibliothèque JavaScript pour interagir avec le DOM. La plupart du temps, nous utilisons une API basée sur jQuery, qu'il s'agisse de jQuery lui-même, Zepto ou encore Ender. Du coup, ce que nous avons sous la main se comporte en fait comme un tableau, et n'est donc jamais équivalent false !

Voyez plutôt :

if ($('#idNotFound')) {
  // Ce code sera TOUJOURS exécuté !
}

C'est la raison pour laquelle on voit tant de code comme celui-ci :

if ($('#idNotFound').length) {
  // Si la longueur est non-nulle (élément trouvé) ce code sera exécuté
}

Paramètre doté d'une valeur par défaut

En CoffeeScript, c'est trivial à gérer, mais en JS pur c'est un peu plus compliqué. Tout dépend en fait des valeurs considérées comme valides pour l'argument.

Si aucune valeur falsy n'est valide, on est tranquille et on peut juste utiliser le || (OU logique) :

function earnMoney(luckyGuy, howMuch) {
  howMuch = howMuch || 10000;
  // Ou parfois : howMuch || howMuch = 10000; // Les goûts et les couleurs…
  console.log(luckyGuy + ' gagne ' + howMuch + '€');
}

Ici, on considère par exemple que howMuch à zéro, NaN, chaîne vide, et simplement non passé (undefined) ne constituerait pas une valeur acceptable. Il n'est donc pas possible d'appeler earnMoney avec un deuxième argument à zéro :

earnMoney('John')           // 'John gagne 10000€'
earnMoney('Nicolas', 1000)  // 'Nicolas gagne 1000€'
earnMoney('Yannick', 0);    // 'Yannick gagne 10000€' !

Lorsque des valeurs falsy sont acceptables, il vous appartient de tester plus efficacement. Par exemple, on pourrait refuser toute valeur négative ou nulle, et gérer évidemment le cas "argument manquant" :

function earnMoney(luckyGuy, howMuch) {
  if ('undefined' == typeof howMuch || +howMuch < 0)
    howMuch = 10000;
  console.log(luckyGuy + ' gagne ' + howMuch + '€');
}

(Notez au passage le recours à l'opérateur unaire préfixe +, qui force le protocole de conversion explicite ToNumber (conversion vers un nombre), et refusera notamment toute valeur textuelle ne constituant pas intégralement un nombre décimal valide. Cette technique est en ce sens supérieure à parseFloat.)

Ici on a bien le comportement souhaité :

earnMoney('John')            // 'John gagne 10000€'
earnMoney('Nicolas', 1000)   // 'Nicolas gagne 1000€'
earnMoney('Yannick', 0);     // 'Yannick gagne 0€'
earnMoney('Matthieu', -10);  // 'Matthieu gagne 10000€'
earnMoney('Marie', '10foo'); // 'Marie gagne 10000€'

Pièges des priorités de conversion

Les conversions implicites peuvent être problématiques. Elles sont déclenchées automatiquement sur toute condition mono-composant et opération binaire autre que === et !== (qui exigent une identité de type, en plus d'une identité de valeur, et sont recommandées par défaut pour toute comparaison, afin de réduire les risques).

Voyez plutôt :

console.log('  ' == false)     // true
if ('  ') console.log('mais…') // 'mais…'

console.log([] == false)     // true
if ([]) console.log('mais…') // 'mais…'

Voilà qui est fort étrange (et y'en a encore plein des comme ça). En fait, il ne s'agit pas ici uniquement de la conversion implicite de String ou Array en booléens, qui n'est utilisé que dans les if du code ci-dessus. Il s'agit aussi de la spécification de l'opérateur ==, telle que décrite au paragraphe 11.9.3 de ECMA-262 (3ème édition), la spécification de JavaScript. Déroulons les parties pertinentes de l'algo utilisé par le moteur JS pour le premier cas, ' ' == false :

  1. On a affaire à un non-booléen comparé à un booléen : on compare ' ' à ToNumber(false)
  2. La conversion implicite numérique d'un booléen vaut +0 (0, quoi…) pour false, on a donc ' ' == 0
  3. On compare une String avec un Number : on compare ToNumber(' ') à 0
  4. La conversion implicite numérique d'une chaîne de caractère constitué uniquement de whitespace vaut (section 9.3.1) 0, on compare donc 0 == 0, ce qui vaut évidemment true.

En revanche, dans le cas du if, on applique immédiatement ToBoolean(' '), et comme la chaîne n'est pas vide, on obtient true.

Subtil ! Et, oui, un peu foireux côté spécification…

Pour le second cas, [] == false, on obtient le même type de déroulé, si ce n'est qu'à l'étape 3 on atteint Object (Array est un type descendant) avec un Number, ce qui déclenche ToPrimitive([]), décrit à la section 9.1. Celle-ci nous dit qu'on utilise alors la propriété système [[DefaultValue]] de l’objet, telle que décrite à la section 8.6.2.6 (remarquez combien ECMA-262 est une spécification pour les implémenteurs, et non pour les développeurs, et comme on s'y emmêle les pieds à coups de références croisées comme dans un code juridique). Les renvois continuent, jusqu'à ce qu'on se rende compte qu'on aboutit à une conversion numérique aboutissant, faute d'élément unique exploitable dans le tableau, à zéro.

Bref, faites attention lorsque vous exploitez une valeur non booléenne comme booléenne : il n'y a pas que la conversion implicite du type d'origine, mais aussi, pour une expression avec opérateurs, les algorithmes de conversion d'opérande définis pour ces opérateurs. Et les deux peuvent, en effet, se contredire.

Envie d’en apprendre davantage ?

Notre formation JS Puissant explore en profondeur tous les aspects techniques avancés du langage JavaScript lui-même, et toute la stack technique web front. De la truthiness à la programmation fonctionnelle en passant par les méthodes méconnues des types natifs et le fonctionnement des prototypes, cette formation JavaScript vous donne un gros coup de boost dans le développement de vos codes JS quels qu’ils soient.