Erreurs courantes de JavaScript et TypeScript qui détériorent silencieusement le code
Les pièges de JavaScript et TypeScript qui semblent corrects mais qui causent encore des problèmes : les comparaisons avec NaN, la coercition de type, les problèmes de synchronisation avec async/await, ainsi que les vérifications d’égalité qui vous trompent.
Sources: 20 JavaScript/TypeScript Mistakes That Look Right… But Break Your App
Pièges liés à la comparaison et à l’égalité
1. NaN n’est jamais égal à rien, pas même à lui-même.
[1, NaN, 3].indexOf(NaN); // -1, even though NaN is right there
Selon les spécifications, NaN === NaN renvoie false ; par conséquent, tout mécanisme basé sur l’égalité stricte — y compris indexOf — échouera à le localiser, même s’il se trouve directement dans l’array. Préférez plutôt Number.isNaN(value), ou utilisez .includes(), conçu spécifiquement pour gérer correctement ce cas particulier.
2. typeof null renvoie "object".
if (typeof value === "object") { value.someMethod(); } // value could be null
C’est une ancienne particularité intégrée au langage, qui piège quiconque considère typeof comme un test fiable pour déterminer s’il s’agit d’un objet. null passe ce contrôle sans problème, mais provoque une erreur dès que l’on tente d’appeler une méthode dessus.
3. La comparaison d’objets via JSON.stringify() est sensible à l’ordre.
JSON.stringify({ a: 1, b: 2 }) === JSON.stringify({ b: 2, a: 1 }); // false
Deux objets représentant les mêmes données peuvent être sérialisés en chaînes de caractères avec des clés dans un ordre différent — ceci se produit naturellement après un accès à une base de données ou une autre partie du code. Évitez la comparaison de chaînes et utilisez plutôt un outil approprié pour vérifier l’égalité profonde.
4. .sort() sans comparateur traite tout comme du texte.
[10, 2, 1].sort(); // [1, 10, 2]
Les valeurs sont d’abord transformées en chaînes de caractères avant d’être comparées caractère par caractère, ce qui perturbe l’ordre numérique. Il faut toujours fournir une fonction de comparaison explicite pour les nombres, telle que (a, b) => a - b.
Coercition de type
5. L’opérateur + passe à la concaténation dès qu’un des opérandes est une chaîne de caractères.
function total(price, quantity) {
return price + quantity; // "5" + 3 = "53" if price came from a form input
}
Les valeurs provenant de paramètres de requête, de champs de formulaire ou de JSON analysé restent souvent des chaînes de caractères même si elles ressemblent à des nombres. L’opérateur + ne convertira pas automatiquement la chaîne en nombre — au contraire, il convertit le nombre en chaîne de caractères.
6. Les valeurs par défaut dans le destructuring ne s’appliquent que lorsque la propriété est undefined, jamais pour null.
function greet({ name = "friend" } = {}) { return `Hi, ${name}`; }
greet({ name: null }); // "Hi, null" — the default never kicks in
La transmission explicite de null est considérée comme la fourniture d’une valeur et non comme son omission, ce qui permet d’éviter complètement l’utilisation de la valeur par défaut. Si les appels peuvent légitimement envoyer null, vous devez le vérifier séparément.
Décalage temporel asynchrone
7. L’exécution de .filter(Boolean) sur un tableau de promesses ne sert à rien.
const results = items.map((item) => checkAsync(item)); // array of Promise objects
const valid = results.filter(Boolean); // every Promise is truthy, filters nothing
Une instance Promise est toujours vraie, quel que soit le résultat final de sa résolution ou de son rejet. Vous devez d’abord les résoudre avec await Promise.all(results), puis seulement filtrer les valeurs obtenues.
8. La chaînage optionnel arrête l’évaluation de toute l’expression, et non seulement de la partie optionnelle.
user?.getPermissions().includes("admin"); // if user is null, this is undefined, not an error, but also never calls .includes at all
Il est tentant de penser que seul le segment ?. est ignoré, mais tout ce qui suit cesse également de s’exécuter. C’est généralement le comportement souhaité, bien qu’un bug subtil apparaisse lorsque une appelation finale — comme une opération d’audit ou d’analyse — n’est jamais exécutée en silence.
9. Promise.all jette tous les résultats dès qu’une seule promesse échoue.
const [a, b, c] = await Promise.all([taskA(), taskB(), taskC()]);
// if taskC rejects, you get nothing back from taskA and taskB either, even if they succeeded
C’est un comportement intentionnel, de type tout ou rien. Lorsque le succès partiel est acceptable dans votre situation, Promise.allSettled renvoie le résultat de chaque promesse — qu’elle ait réussi ou échoué — au lieu d’ignorer les résultats positifs à cause de l’échec d’une tâche.
Mutation des objets et des tableaux
10. const verrouille la liaison, pas l’objet auquel elle fait référence.
const config = { retries: 3 };
config.retries = 10; // completely legal, const never protected this
Déclarer une variable avec const ne vous empêche que de la réaffecter à une valeur différente. L’objet auquel elle fait référence reste tout aussi modifiable que si vous aviez utilisé let. Si vous avez besoin d’une véritable immuabilité, utilisez Object.freeze()—et gardez à l’esprit qu’il ne gèle que le niveau le plus profond.
11. .sort() modifie l’array d’origine, contrairement à .map() ou .filter().
const original = [3, 1, 2];
const sorted = original.sort();
console.log(original); // [1, 2, 3] — also changed, probably not what was intended
Il est facile de penser que toutes les méthodes d’array retournent un nouvel array, puisque la plupart des méthodes courantes le font effectivement. .sort() et .reverse() sont les exceptions, et modifier un array auquel une autre partie de votre code fait encore référence entraîne l’erreur classique « cette valeur a changé toute seule ».
12. for...in itère également sur les propriétés héritées, et non seulement sur les clés propres de l’objet.
Array.prototype.customMethod = function () {};
const arr = [1, 2, 3];
for (const key in arr) { console.log(key); } // includes "customMethod" too
Cela ne pose presque jamais de problème en pratique, car l’extension des prototypes intégrés est rare, mais toute propriété énumérable héritée de la chaîne de prototypes apparaîtra dans une boucle for...in. L’utilisation de Object.keys() ou de for...of évite complètement cette surprise.
Fonctions et portée
13. Les fonctions flèche ne lient jamais leur propre this.
const counter = {
count: 0,
increment: () => { this.count++; }, // `this` here is NOT the counter object
};
Une fonction flèche récupère this dans la portée lexique où elle a été écrite, et non dans l’objet qui finit par l’appeler. Cela en fait des choix idéaux pour les fonctions de rappel qui doivent conserver un this externe, mais un mauvais choix pour les méthodes d’objet ou de classe qui doivent faire référence à leur propre instance.
14. Une instruction switch se poursuit silencieusement lorsque l’on omet break.
switch (status) {
case "pending":
console.log("Order is pending");
case "shipped": // no break above, this runs too
console.log("Order has shipped");
}
En omettant un seul break, chaque cas suivant s’exécute, que sa condition soit ou non remplie. Un tel comportement intentionnel existe parfois, mais il est si rare que l’absence de break indique presque toujours une erreur plutôt qu’un choix de conception.
Dates et nombres
15. 0.1 + 0.2 n’est pas égal à 0.3.
0.1 + 0.2 === 0.3; // false
0.1 + 0.2; // 0.30000000000000004
Cela provient de la représentation standard des nombres à virgule flottante et concerne presque tous les langages, ce n’est pas propre à JavaScript. Évitez d’utiliser l’égalité stricte pour des valeurs décimales calculées ; vérifiez plutôt si la différence entre deux nombres se situe dans une tolérance acceptable.
16. Les chaînes de dates sans précision horaire sont interprétées en UTC, tandis que les dates séparées par des barres obliques sont interprétées selon l’heure locale.
new Date("2024-01-01"); // midnight UTC
new Date("01/01/2024"); // midnight local time
Deux chaînes qui semblent décrire le même jour peuvent correspondre à des dates calendaires différentes en fonction du fuseau horaire de la personne qui exécute le code. Choisissez un format de date et restez-y constamment, et privilégiez une bibliothèque de dates éprouvée plutôt que d’analyser manuellement les chaînes.
Pièges spécifiques à TypeScript
17. Les assertions de type ne valident rien en temps de exécution.
const user = JSON.parse(rawData) as User; // TypeScript trusts you, nothing is actually checked
Écrire as User indique au compilateur de vous faire entièrement confiance, et il le fait sans effectuer aucune vérification pour s’assurer que la valeur analysée correspond bien à la forme que vous avez indiquée. Des données incorrectes ou inattendues continuent de circuler dans votre programme en étant considérées comme validées, jusqu’à ce qu’elles provoquent finalement une erreur ailleurs dans le flux de traitement.
18. L’opérateur d’affirmation non nul ne fait que supprimer les avertissements, il ne change pas la réalité.
function getName(user?: User) {
return user!.name; // TypeScript stops warning you; the runtime null check is still gone
}
L’ajout de ! indique à TypeScript que vous êtes certain qu’une valeur n’est ni null ni undefined. Si cette supposition est fausse, le compilateur cesse simplement de signaler ce risque, et l’erreur apparaît en temps de exécution, là où le système de types aurait dû la détecter.
19. La vérification des propriétés en trop s’applique uniquement aux littéraux d’objet passés directement, et non aux variables ayant la même structure.
type Config = { retries: number };
function setup(config: Config) {}
setup({ retries: 3, timeout: 5000 }); // error: 'timeout' does not exist on type Config
const options = { retries: 3, timeout: 5000 };
setup(options); // no error — same excess property, but it slips through
En passant directement un objet littéral dans une appel de fonction, TypeScript examine chaque propriété et rejette tout élément supplémentaire. En attribuant d’abord cet même objet à une variable avant de passer cette dernière, la vérification disparaît complètement. Il s’agit d’une particularité réelle du comportement du vérificateur de types, ce qui signifie qu’une faute d’orthographe ou un champ supplémentaire non prévu peut passer inaperçu lors de la validation, en fonction uniquement du mode d’entrée de la valeur dans la fonction.
20. Les enums numériques acceptent des nombres arbitraires, et non seulement leurs membres déclarés.
enum Status { Pending, Shipped, Delivered }
function process(status: Status) {}
process(999); // no compile error, even though 999 isn't a declared Status value
Au niveau technique, les enums numériques en TypeScript ne sont rien d’autre que de simples nombres, ce qui signifie que le système de types accepte n’importe quelle valeur numérique comme valide, indépendamment du fait qu’elle corresponde ou non à un membre déclaré. Les enums de chaînes imposent quant à elles des restrictions réelles, ce qui vous incite à utiliser des enums de chaînes ou une union composée de littéraux de chaîne lorsque vous souhaitez que le compilateur rejette effectivement les entrées incorrectes.
Aucun de ces vingt problèmes n’est obscur ou exotique. Chacun d’eux est suffisamment courant pour apparaître dans du code réel, soumis à des revues et mis en production, précisément parce qu’il ressemble parfaitement à du JavaScript ou TypeScript ordinaire jusqu’à ce que des entrées particulières, des conditions temporelles ou des cas limites le révèlent. Être conscient de cette liste ne remplace pas la nécessité de tester soigneusement votre code. Ce qu’elle vous offre, c’est un avantage : vous vérifiez à l’avance les problèmes pour lesquels vous savez qu’il faut faire attention, plutôt que de les découvrir à posteriori en production.