Objectifs d'apprentissage
À la fin de ce cours, l'apprenant peut :
- déclarer des variables avec
constetlet, et prévoir le résultat d'une coercition ou d'une comparaison ; - écrire des fonctions (déclaration, flèche), expliquer la portée, les fermetures (closures) et la valeur de
this; - manipuler objets, classes et tableaux (
map,filter,reduce), avec destructuration et spread ; - organiser du code en modules ES (
import/export) ; - expliquer la boucle d'événements et ordonner pile, micro-tâches et tâches ;
- écrire du code asynchrone avec promesses et
async/await, et gérer les erreurs ; - lire et produire du JSON sans exécuter de données ;
- distinguer l'exécution dans le navigateur de celle dans Node.js ;
- utiliser npm avec méthode : fichier de verrouillage,
npm ci, audit, scripts d'installation ; - reconnaître et éviter les failles courantes : XSS,
innerHTML,eval, entrée non fiable ; - typer un programme TypeScript : types, interfaces, génériques, unions et réduction de type,
strict; - éviter les pièges classiques du langage.
Prérequis
- Savoir ouvrir Terminal et un éditeur de texte (cours « Terminal macOS » et « VS Code »).
- Notions de programmation (variable, condition, boucle). Les notions JavaScript sont reprises depuis le début.
- Node.js, npm et un navigateur récent pour les exercices, et le compilateur TypeScript pour le chapitre 13 : versions non vérifiées sur le poste de Stéphane. Ce cours n'enseigne pas leur installation et n'installe rien.
- Un dossier de test jetable. Aucun exercice ne demande de droits administrateur ni de compte.
Parcours et chapitres
1. JavaScript, ECMAScript, TypeScript et où le code s'exécute
JavaScript est le langage ; ECMAScript est sa spécification, publiée par Ecma International et travaillée par le comité TC39 (le brouillon lu sur tc39.es est intitulé « ECMAScript 2027 »). Le langage s'exécute dans un hôte qui lui ajoute des fonctions : le navigateur (DOM, fetch, localStorage) ou Node.js (fichiers, processus, réseau). TypeScript est une surcouche : un langage qui ajoute des types à JavaScript. Le compilateur vérifie les types puis produit du JavaScript ; les types sont effacés à l'exécution (voir chapitre 13).
| Terme | Sens |
|---|---|
| ECMAScript | la norme du langage (syntaxe, objets de base) |
| JavaScript | le langage, tel que l'implémentent les moteurs |
| Hôte (navigateur, Node.js) | fournit les API qui ne sont pas dans la norme (DOM, fichiers) |
| TypeScript | JavaScript plus un système de types vérifié avant l'exécution |
À retenir : une fonction comme document.querySelector ou fs.readFile n'appartient pas au langage mais à l'hôte. Le même fichier peut donc marcher dans Node.js et échouer dans le navigateur, et inversement (chapitre 10).
2. Variables, types, coercition et égalité
const, let et var
Utiliser const par défaut, let quand la variable doit être réaffectée, et ne pas écrire var dans du code neuf. const interdit de réaffecter la variable, pas de modifier l'objet qu'elle désigne. let et const ont une portée de bloc ; var une portée de fonction (comportement général du langage, non relu sur MDN pour ce cours).
const prix = 12;
let stock = 3;
stock = stock - 1;
const panier = { articles: [] };
panier.articles.push("stylo");
// panier = {}; // TypeError : on ne réaffecte pas une constante
console.log(prix, stock, panier);- Résultat attendu :
12 2 { articles: [ 'stylo' ] }; la ligne commentée provoquerait une erreur si elle était active.
Types primitifs et objets
Les primitifs sont string, number, bigint, boolean, undefined, null et symbol ; tout le reste est un objet (tableaux, fonctions, dates). typeof renvoie le nom du type, avec une exception historique connue : typeof null vaut "object" (comportement connu, non relu ici). NaN est un number ; les nombres sont des flottants, donc 0.1 + 0.2 ne vaut pas exactement 0.3.
console.log(typeof 42, typeof "texte", typeof undefined, typeof null, typeof [], typeof (() => 1));
console.log(0.1 + 0.2 === 0.3, Number.isNaN(NaN), Array.isArray([]));- Résultat attendu :
number string undefined object object function, puisfalse true true.
Coercition et égalité
La coercition est la conversion implicite d'une valeur. MDN décrit trois comparaisons : == (égalité souple, avec conversion), === (égalité stricte, sans conversion, types identiques exigés) et Object.is(). Règles de == rappelées par MDN : null == undefined est vrai et ces deux valeurs ne sont égales qu'entre elles ; un booléen est converti en nombre ; une chaîne est convertie en nombre pour la comparaison avec un nombre. Object.is se distingue de === sur deux cas : NaN y est égal à lui-même, et +0 n'y est pas égal à -0.
| Expression | Résultat | Cause |
|---|---|---|
"0" == 0 | true | la chaîne devient un nombre |
0 == false | true | le booléen devient un nombre |
"0" === 0 | false | types différents |
null == undefined | true | règle particulière de == |
NaN === NaN | false | seul Object.is(NaN, NaN) est vrai |
"5" + 1 | "51" | + concatène dès qu'une chaîne est présente (comportement général, non relu) |
"5" - 1 | 4 | - convertit en nombre (idem) |
À retenir : écrire === et !== partout. La seule exception raisonnable est valeur == null, qui teste à la fois null et undefined (TypeScript la reconnaît aussi, chapitre 14).
Valeurs « falsy » (évaluées comme faux dans une condition) : false, 0, "", null, undefined, NaN (la page TypeScript sur la réduction de type liste 0, NaN, "", null, undefined).
Attention : if (quantite) traite 0 comme « absent ». Si 0 est une valeur valable, tester explicitement quantite !== undefined, ou utiliser ?? (valeur par défaut seulement pour null ou undefined) plutôt que ||.
const quantite = 0;
console.log(quantite || 10); // 10 : 0 est falsy
console.log(quantite ?? 10); // 0 : seulement null ou undefined déclenchent la valeur par défaut
console.log(Number("12"), Number("douze"), parseInt("12 stylos", 10));- Résultat attendu :
10, puis0, puis12 NaN 12.
3. Fonctions, portée, closures et this
Trois écritures
function additionner(a, b) {
return a + b;
}
const multiplier = function (a, b) {
return a * b;
};
const soustraire = (a, b) => a - b;
const carre = (x) => x * x;
console.log(additionner(2, 3), multiplier(2, 3), soustraire(5, 3), carre(4));- Résultat attendu :
5 6 2 16.
Une fonction est une valeur : on la passe en argument, on la renvoie, on la range dans un tableau. Valeurs par défaut (function f(x = 1)) et paramètre du reste (...args) sont disponibles.
Portée et fermeture (closure)
La portée est l'endroit d'où une variable est visible. Une fermeture est une fonction qui garde l'accès aux variables de la portée où elle a été créée, même après la fin de cette portée.
function creerCompteur() {
let n = 0;
return () => {
n += 1;
return n;
};
}
const compteur = creerCompteur();
console.log(compteur(), compteur(), compteur());- Résultat attendu :
1 2 3. Chaque appel decreerCompteur()crée unndistinct.
this
D'après MDN, la valeur de this dépend de la façon dont la fonction est appelée, pas de l'endroit où elle est écrite :
| Situation | Valeur de this |
|---|---|
appel de méthode obj.f() | obj |
appel simple f() | undefined en mode strict, objet global sinon |
| fonction flèche | celui de la portée englobante, figé ; bind, call, apply n'y changent rien |
new F() | le nouvel objet créé |
| méthode de classe | l'instance (méthode statique : la classe) |
bind(obj) | nouvelle fonction avec this fixé |
| gestionnaire d'événement DOM | l'élément sur lequel l'écouteur est attaché |
| haut niveau d'un module | undefined |
"use strict";
const boutique = {
nom: "Papeterie fictive",
direBonjour() {
return `Bienvenue chez ${this.nom}`;
},
};
console.log(boutique.direBonjour());
const detachee = boutique.direBonjour;
try {
console.log(detachee());
} catch (e) {
console.log("Erreur :", e.constructor.name);
}- Résultat attendu : la première ligne affiche la phrase ; la seconde appelle la méthode sans objet,
thisvautundefinedet unTypeErrorest capté. La méthode détachée « perd » son objet.
Astuce : pour un rappel qui doit garder le this de la méthode qui le crée, utiliser une fonction flèche. Pour une méthode d'objet ou de classe, utiliser la syntaxe de méthode, pas une flèche.
4. Objets, prototypes en bref et classes
Un objet est une collection de propriétés. Chaque objet a un prototype : si une propriété manque, le moteur la cherche dans le prototype, puis dans le prototype du prototype (la chaîne de prototypes). C'est le mécanisme d'héritage de JavaScript ; les classes en sont une syntaxe plus lisible (explication générale, non relue sur MDN pour ce cours).
const animal = { cri() { return "..."; } };
const chien = Object.create(animal);
chien.nom = "Rex";
console.log(chien.cri(), Object.hasOwn(chien, "cri"), Object.getPrototypeOf(chien) === animal);- Résultat attendu :
... false true:criest trouvée par le prototype, elle n'appartient pas àchien.
class Compte {
#solde = 0; // champ privé (syntaxe #)
constructor(titulaire) {
this.titulaire = titulaire;
}
deposer(montant) {
if (!Number.isFinite(montant) || montant <= 0) {
throw new RangeError("montant invalide");
}
this.#solde += montant;
}
get solde() {
return this.#solde;
}
}
class CompteEpargne extends Compte {
constructor(titulaire, taux) {
super(titulaire);
this.taux = taux;
}
}
const c = new CompteEpargne("Titulaire fictif", 0.02);
c.deposer(100);
console.log(c.solde, c instanceof Compte);- Résultat attendu :
100 true.c.#soldehors de la classe serait une erreur de syntaxe. Champs privés#: syntaxe du langage, non relue sur MDN pour ce cours.
Attention : class ne crée pas un « vrai » modèle objet à la Java : c'est une syntaxe sur les prototypes. Une méthode passée en rappel perd son this (chapitre 3). Préférer la composition à de longues hiérarchies d'héritage.
5. Tableaux, méthodes, destructuration et spread
Les méthodes map, filter et reduce ne modifient pas le tableau d'origine ; elles en construisent un autre (comportement général, non relu sur MDN pour ce cours). push, pop, splice et sort modifient le tableau en place.
const ventes = [
{ article: "stylo", quantite: 3, prix: 2 },
{ article: "cahier", quantite: 1, prix: 5 },
{ article: "gomme", quantite: 0, prix: 1 },
];
const vendues = ventes.filter((v) => v.quantite > 0);
const lignes = vendues.map((v) => ({ article: v.article, total: v.quantite * v.prix }));
const chiffre = lignes.reduce((somme, l) => somme + l.total, 0);
console.log(lignes, chiffre);
console.log(ventes.find((v) => v.article === "cahier")?.prix);
console.log(ventes.some((v) => v.quantite === 0), ventes.every((v) => v.prix > 0));- Résultat attendu : deux lignes (
stylo6,cahier5),11, puis5, puistrue true.
Attention : sort() sans fonction de comparaison trie en chaînes de caractères (comportement connu, non relu) : [10, 2, 1].sort() donne [1, 10, 2]. Pour trier des nombres : .sort((a, b) => a - b). Et sort modifie le tableau : copier d'abord ([...tableau].sort(...)).
Destructuration et spread
const { article, prix = 1, ...reste } = { article: "stylo", quantite: 3 };
const [premier, deuxieme = "aucun", ...autres] = ["a", undefined, "c", "d"];
const copie = { ...reste, article: "crayon" }; // copie superficielle, propriété remplacée
const fusion = [...[1, 2], ...[3, 4]];
console.log(article, prix, reste, premier, deuxieme, autres, copie, fusion);- Résultat attendu :
stylo 1 { quantite: 3 } a aucun [ 'c', 'd' ] { quantite: 3, article: 'crayon' } [ 1, 2, 3, 4 ].
Attention : le spread fait une copie superficielle : les objets imbriqués restent partagés. Pour une copie profonde, structuredClone existe dans les navigateurs récents et dans Node.js (disponibilité par version non vérifiée).
6. Modules ES
Un module est un fichier qui exporte des noms et en importe d'autres. D'après MDN, import est une déclaration statique : à la racine du fichier, avec un spécificateur en chaîne littérale ; les imports sont des liaisons en lecture seule et vivantes (live bindings) ; un module est automatiquement en mode strict ; l'import dynamique import() charge un module à la demande ; les modules acceptent l'await de haut niveau. Les imports sont « hissés » : leurs effets se produisent avant le reste du code.
// fichier maths.mjs
export const TVA = 0.2;
export function ttc(ht) {
return ht * (1 + TVA);
}
export default function arrondir(x) {
return Math.round(x * 100) / 100;
}// fichier principal.mjs
import arrondir, { ttc, TVA as taux } from "./maths.mjs";
import * as maths from "./maths.mjs";
console.log(arrondir(ttc(10)), taux, typeof maths.ttc);
const chargement = await import("./maths.mjs"); // import dynamique (await de haut niveau)
console.log(chargement.default(1.2345));- Résultat attendu :
12 0.2 function, puis1.23. Les chemins relatifs commencent par./.
Spécificateurs (MDN) : relatif (./x.js), absolu (URL complète, node:), nu (nom de paquet : demande une carte d'import dans le navigateur, ou la résolution de Node.js / d'un outil de build).
Dans Node.js (page « ECMAScript modules », v26.12.0) : .mjs est toujours un module ES, .cjs toujours CommonJS ; pour .js, le champ "type" de package.json décide ("module" ou "commonjs", ce dernier par défaut), avec une détection de syntaxe en dernier recours. __dirname n'existe pas dans un module ES : utiliser import.meta.dirname ou import.meta.filename. require() ne sait charger que des modules ES synchrones (sans await de haut niveau). Dans le navigateur : <script type="module" src="...">.
Astuce : commencer un nouveau projet Node.js en .mjs ou avec "type": "module", pour avoir un seul système de modules. Le mélange CommonJS / ES est une source d'erreurs fréquente.
7. Asynchrone : callbacks, promesses, async/await et boucle d'événements
La boucle d'événements
JavaScript exécute le code d'un programme sur un seul fil. MDN décrit le modèle d'exécution ainsi :
- la pile d'appels (LIFO) contient les appels de fonction en cours ;
- la file de tâches reçoit les rappels de minuteurs, d'entrées-sorties et d'événements utilisateur ;
- la file de micro-tâches, prioritaire, reçoit les rappels de promesses (
then,catch),queueMicrotasket lesMutationObserver; - exécution jusqu'à la fin (run-to-completion) : un travail va au bout avant que le suivant ne commence ;
- quand la pile est vide, toutes les micro-tâches sont traitées, avant qu'une nouvelle tâche soit prise. Une promesse déjà résolue passe donc avant un
setTimeout(…, 0).
console.log("1 synchrone");
setTimeout(() => console.log("4 tâche (minuteur 0 ms)"), 0);
Promise.resolve().then(() => console.log("3 micro-tâche (promesse)"));
console.log("2 synchrone");- Résultat attendu :
1,2,3,4dans cet ordre : le code synchrone d'abord, puis les micro-tâches, puis la tâche.
Attention : un calcul long dans un travail bloque tout le reste (page figée dans le navigateur, serveur muet dans Node.js), car rien ne l'interrompt. Découper le travail, ou le confier à un autre processus ou un worker (non traité ici).
Callbacks
Un rappel (callback) est une fonction passée pour être appelée plus tard. C'est le style historique ; imbriqués, les rappels forment la « pyramide de l'enfer » et rendent la gestion d'erreur pénible. Les promesses les remplacent.
Promesses
Une promesse a trois états : en attente (pending), tenue (fulfilled) ou rejetée (rejected) ; une fois réglée, elle ne change plus. then renvoie une nouvelle promesse ; une exception dans un then la rejette ; catch est un raccourci pour then(null, …) ; finally s'exécute dans tous les cas et ne reçoit pas la valeur. MDN décrit quatre combinateurs :
| Méthode | Comportement |
|---|---|
Promise.all([...]) | tenue quand toutes le sont ; rejetée dès qu'une est rejetée |
Promise.allSettled([...]) | attend que toutes soient réglées, quel qu'en soit le résultat |
Promise.race([...]) | se règle comme la première réglée |
Promise.any([...]) | tenue dès que l'une est tenue ; rejetée si toutes sont rejetées |
async / await
async / await est une écriture, fondée sur les promesses, qui ressemble à du code synchrone. Une fonction async renvoie toujours une promesse ; await suspend la fonction (pas le programme) jusqu'au règlement de la promesse ; une exception levée rejette la promesse de la fonction, et try / catch capte les rejets des await.
function attendre(ms, valeur, echec = false) {
return new Promise((resolve, reject) => {
setTimeout(() => (echec ? reject(new Error("échec simulé : " + valeur)) : resolve(valeur)), ms);
});
}
async function principal() {
const a = await attendre(50, "A");
console.log("séquentiel :", a);
const [b, c] = await Promise.all([attendre(50, "B"), attendre(50, "C")]); // en parallèle
console.log("parallèle :", b, c);
const bilan = await Promise.allSettled([attendre(10, "D"), attendre(10, "E", true)]);
console.log(bilan.map((r) => r.status));
try {
await attendre(10, "F", true);
} catch (erreur) {
console.log("capté :", erreur.message);
}
}
principal().catch((e) => {
console.error("erreur non prévue :", e);
process.exitCode = 1; // Node.js ; supprimer cette ligne dans un navigateur
});- Résultat attendu (Node.js) :
séquentiel : A,parallèle : B C,[ 'fulfilled', 'rejected' ],capté : échec simulé : F.
Attention : les pièges classiques sont (1) oublier await (on manipule alors une promesse, pas sa valeur), (2) enchaîner des await indépendants l'un après l'autre alors qu'un Promise.all irait plus vite, (3) ne pas attacher de catch : un rejet non géré est signalé, et peut arrêter un processus Node.js (comportement par version : non vérifié), (4) mettre un await dans forEach, qui n'attend pas (utiliser for…of).
8. Erreurs et JSON
Erreurs
On lève une erreur avec throw (de préférence un objet Error ou une de ses sous-classes, qui porte un message et une trace) et on la capte avec try / catch / finally. On ne capte que ce qu'on sait traiter ; on relance le reste.
class ErreurValidation extends Error {
constructor(message) {
super(message);
this.name = "ErreurValidation";
}
}
function lireAge(texte) {
const age = Number(texte);
if (!Number.isInteger(age) || age < 0) {
throw new ErreurValidation(`âge invalide : ${JSON.stringify(texte)}`);
}
return age;
}
for (const entree of ["42", "abc", "-1"]) {
try {
console.log(lireAge(entree));
} catch (e) {
if (e instanceof ErreurValidation) {
console.log("refusé :", e.message);
} else {
throw e; // erreur inattendue : on ne l'avale pas
}
} finally {
// nettoyage éventuel, exécuté dans tous les cas
}
}- Résultat attendu :
42, puis deux lignes « refusé ».
À retenir : une erreur levée dans une fonction asynchrone ne se capte pas avec un try placé autour de l'appel si l'on a oublié await : l'erreur devient un rejet de promesse (chapitre 7).
JSON
JSON est un format texte d'échange. JSON.stringify transforme une valeur en texte, JSON.parse fait l'inverse et lève une exception si le texte est invalide (type exact de l'erreur : SyntaxError, comportement général non relu sur MDN). Le JSON n'a ni undefined, ni fonctions, ni dates : une Date devient une chaîne.
const brut = '{"nom":"Dupont fictif","quantite":3,"tags":["a","b"]}';
try {
const donnees = JSON.parse(brut);
console.log(donnees.quantite + 1, Array.isArray(donnees.tags));
console.log(JSON.stringify({ ok: true, date: new Date(0), absent: undefined }, null, 2));
} catch (e) {
console.log("JSON invalide :", e.name);
}
try {
JSON.parse("{pas du json}");
} catch (e) {
console.log("capté :", e.name);
}- Résultat attendu :
4 true, un JSON indenté oùdateest une chaîne etabsenta disparu, puiscapté : SyntaxError.
Attention : JSON.parse ne valide pas la forme des données : après l'analyse, vérifier que les champs attendus existent et ont le bon type (chapitre 12). Jamais d'eval pour lire du JSON.
9. DOM et événements, en bref
Dans le navigateur, le DOM (Document Object Model) représente la page comme un arbre d'objets. On sélectionne un élément (document.querySelector), on lit ou change son contenu, et on réagit aux événements avec addEventListener. Les événements remontent du plus profond au plus englobant (propagation), ce qui permet la délégation d'événement : un seul écouteur sur le parent.
<!doctype html>
<meta charset="utf-8">
<title>Essai DOM (données fictives)</title>
<input id="saisie" placeholder="écrire ici">
<button id="ajouter">Ajouter</button>
<ul id="liste"></ul>
<script type="module">
const saisie = document.querySelector("#saisie");
const liste = document.querySelector("#liste");
document.querySelector("#ajouter").addEventListener("click", () => {
const item = document.createElement("li");
item.textContent = saisie.value; // texte, jamais interprété comme du HTML
liste.append(item);
saisie.value = "";
});
liste.addEventListener("click", (evenement) => { // délégation : un écouteur pour tous les <li>
if (evenement.target instanceof HTMLLIElement) {
evenement.target.remove();
}
});
</script>- Résultat attendu : ouvert dans un navigateur, chaque clic sur « Ajouter » ajoute une ligne ; un clic sur une ligne la retire.
À retenir : pour écrire du texte dans la page, textContent ; pas innerHTML (chapitre 12). Un gestionnaire reçoit l'objet événement (target, preventDefault(), stopPropagation()).
10. Navigateur et Node.js : les différences
| Sujet | Navigateur | Node.js (v26.12.0 dans la documentation lue) |
|---|---|---|
| Objet global | window (globalThis) | globalThis ; pas de window ni de document |
| Page, DOM | oui | non |
| Fichiers | pas d'accès libre au disque | module node:fs |
| Arguments, environnement | sans objet | process.argv, process.env |
| Code de sortie | sans objet | process.exitCode |
| Modules | <script type="module"> | .mjs, "type", CommonJS (require) |
| Sécurité | bac à sable du navigateur, politique de même origine | le programme a les droits de l'utilisateur |
La page process de Node.js indique : process.argv[0] est le chemin de l'exécutable, argv[1] celui du script, les suivants sont les arguments ; process.env expose les variables d'environnement ; process.exitCode fixe le code de sortie sans arrêter brutalement. Elle mentionne aussi une API de modèle de permissions (process.permission) marquée expérimentale (stabilité 1) : ne pas s'y fier pour de la sécurité sans vérifier son état pour la version utilisée.
Attention : un script Node.js n'est pas confiné. Un paquet npm ou un fichier .js exécuté par Node.js peut lire vos fichiers, appeler le réseau et lancer des programmes. Dans le navigateur, le code d'une page est limité par le bac à sable ; sur un poste de développement, il ne l'est pas (chapitre 11).
// fichier salutations.mjs — à lancer avec : node salutations.mjs Alice
import { argv } from "node:process";
const prenom = argv[2];
if (!prenom) {
console.error("usage : node salutations.mjs <prénom>");
process.exitCode = 1;
} else {
console.log(`Bonjour ${prenom}`);
}node salutations.mjs Alicenode salutations.mjsecho $?Bloc actif : la touche C copie la commande seule, sans invite ni sortie.
- Contexte : Terminal, dossier de test jetable contenant
salutations.mjs; Node.js installé (version non vérifiée). - Droits : utilisateur normal ; aucune écriture, aucun accès réseau.
- Effet : le premier appel affiche
Bonjour Alice; le second affiche l'usage surstderret positionne le code de sortie. - Vérification :
echo $?affiche1après le second appel (et0après le premier). - Retour arrière : sans objet (lecture seule) ; supprimer le fichier de test.
11. npm et les risques de chaîne d'approvisionnement
npm est le gestionnaire de paquets de Node.js et le dépôt public associé. Un projet déclare ses dépendances dans package.json ; npm résout l'arbre complet (dépendances de dépendances) et le fige dans package-lock.json.
{
"name": "essai-fictif",
"version": "0.0.1",
"private": true,
"type": "module",
"scripts": {
"start": "node principal.mjs",
"test": "node --test"
}
}Un projet moyen embarque des centaines de paquets écrits par des inconnus (ordre de grandeur courant, non vérifié) : la chaîne d'approvisionnement (supply chain) est donc la première surface d'attaque d'un projet JavaScript.
| Risque | Explication | Parade |
|---|---|---|
| Scripts d'installation | npm exécute preinstall, install, postinstall, prepare… des paquets pendant npm install (documentation npm v11.21.0), donc du code arbitraire sur votre machine | --ignore-scripts pour une première analyse ; relire avant d'autoriser |
| Versions flottantes | ^1.2.0 accepte des versions plus récentes à l'installation suivante | fichier de verrouillage versionné, npm ci |
| Paquet piégé ou à nom trompeur (typosquatting) | un nom proche d'un paquet connu (risque général, non relu dans la documentation npm) | vérifier le nom exact, le dépôt, la popularité ; moins de dépendances |
| Dépendance compromise | un mainteneur ou un compte est piraté | verrou, audit, vérification des signatures |
| Vulnérabilité connue | faille publiée dans une dépendance | npm audit |
npm ci contre npm install (page npm ci) : npm ci est conçu pour les environnements automatisés ; il exige un package-lock.json (ou npm-shrinkwrap.json), échoue si les dépendances ne correspondent pas au verrou au lieu de le modifier, supprime node_modules avant d'installer et n'écrit jamais dans package.json ni dans le verrou.
npm audit (page npm audit) envoie les informations de dépendances au registre, qui renvoie un rapport de vulnérabilités connues. Il exige un fichier de verrouillage par défaut. npm audit fix applique les mises à jour compatibles (il lance un npm install complet : les scripts s'exécutent) ; --force autorise des changements de version majeure, à ne jamais lancer sans relire. npm audit signatures vérifie les signatures du registre et les attestations de provenance. Le code de sortie est 0 s'il n'y a rien à signaler, non nul sinon (selon --audit-level). Le rapport ne détecte que les failles déjà publiées : un audit propre ne prouve pas l'absence de code malveillant.
Attention : installer un paquet revient à exécuter le code de son auteur avec vos droits, immédiatement si le paquet a un script d'installation. Les commandes ci-dessous n'ont de sens que dans un dossier jetable.
Attention, effet notable. npm install télécharge et peut exécuter du code tiers : dossier jetable, pas de secret sur la machine Lire le contexte, les droits et l’effet attendu indiqués après le bloc ; ne jamais l’exécuter sur un système réel sans validation, d’abord dans un environnement de test.
npm install --ignore-scripts nom-du-paquetBloc actif : la touche C copie la commande seule, sans invite ni sortie.
- Contexte : Terminal, dossier de test contenant un
package.json; npm installé (version non vérifiée) ; accès réseau au registre ;nom-du-paquetest un paramètre fictif à remplacer. - Droits : utilisateur normal ; jamais
sudo npm. - Effet : télécharge le paquet et ses dépendances dans
node_modules, met à jourpackage.jsonetpackage-lock.json;--ignore-scriptsempêche l'exécution des scripts d'installation (l'option existe, d'après la pagenpm ci; son effet exact sur chaque commande n'a pas été relu). - Vérification :
npm lsliste l'arbre installé ; relire le diff depackage-lock.json. - Retour arrière :
npm uninstall nom-du-paquet, ou supprimernode_moduleset restaurerpackage.jsonetpackage-lock.json(idéalement versionnés avec git).
npm cinpm auditBloc actif : la touche C copie la commande seule, sans invite ni sortie.
- Contexte : Terminal, dossier de projet de test avec un
package-lock.json; npm (version non vérifiée) ; réseau. - Droits : utilisateur normal.
- Effet :
npm ciinstalle exactement ce que fige le verrou (et exécute les scripts d'installation des dépendances, sauf--ignore-scripts) ;npm auditenvoie les données de dépendances au registre et affiche un rapport, sans rien modifier. - Vérification :
npm cise termine sans erreur de désynchronisation ;echo $?aprèsnpm auditdonne0ou non nul selon les résultats. - Retour arrière : supprimer
node_modules; le verrou n'a pas été modifié.
Attention : ne jamais exécuter un curl … | sh ou un script d'installation trouvé sur Internet sans l'avoir lu : c'est la même exécution de code non relu. Cette consigne vaut aussi pour les npx un-paquet (qui télécharge puis exécute).
Bonnes pratiques :
- versionner
package-lock.json; en intégration continue,npm ci; - peu de dépendances, de préférence connues et entretenues ; préférer la bibliothèque standard de Node.js quand elle suffit ;
- relire le diff du verrou à chaque mise à jour ; mettre à jour par petits lots ;
- aucun secret dans
package.json, le code ou les scripts ; ne pas installer enroot; - lancer
npm auditrégulièrement, en sachant ses limites.
12. Sécurité du code JavaScript
Règle de base : ne jamais faire confiance à une entrée. Est une entrée tout ce qui ne vient pas de votre code : champ de formulaire, paramètre d'URL, localStorage, réponse d'une API, fichier JSON, contenu d'un paquet, variable d'environnement, argument de ligne de commande.
XSS et innerHTML
Le cross-site scripting (XSS) est l'injection de script dans une page. MDN (page innerHTML) l'explique : innerHTML analyse son entrée comme du HTML ; un <script> injecté ne s'exécute pas, mais un attribut gestionnaire d'événement le fait (exemple de la page : une image en erreur avec un attribut onerror). Recommandations de MDN : utiliser textContent pour du texte brut ; sinon passer par des objets Trusted Types et une bibliothèque d'assainissement, imposer require-trusted-types-for 'script' par la politique de sécurité de contenu (CSP) ; MDN cite aussi Element.setHTML() et l'API Sanitizer (l'état de prise en charge de ces API : non vérifié).
// Dans une page : donnée non fiable simulée (fictive)
const entree = "<img src=x onerror=console.log('code injecté')>";
const zone = document.querySelector("#zone");
zone.textContent = entree; // sûr : le texte est affiché tel quel
// zone.innerHTML = entree; // dangereux : le gestionnaire s'exécuterait- Résultat attendu : la page affiche la chaîne comme du texte ; rien ne s'exécute. Décommenter la ligne
innerHTML(dans une page de test) montrerait l'injection.
Injection et eval
eval(texte)etnew Function(texte)exécutent du texte comme du code : à proscrire avec toute donnée extérieure. Pour des données,JSON.parse; pour une table de correspondance, un objet ou unMap.- Chaînes de commande : `
child_process.exec(ls ${entree})passe par un shell et permet l'injection de commande (même principe quesubprocessavecshell=Trueen Python). PréférerexecFileouspawn` avec une liste d'arguments (API Node.js : non relue pour ce cours). - Requêtes SQL : paramètres liés du pilote, jamais de concaténation (cours « SQL »).
- URL : valider le schéma avant de l'utiliser dans
hrefoulocation(javascript:est un piège connu). - Objets : fusionner des données externes dans un objet existant peut permettre la pollution de prototype (écriture de
__proto__) ; valider, ou utiliserObject.create(null)/Mappour des clés venant de l'extérieur (risque connu, non relu dans les sources officielles consultées).
Valider une entrée
function lireCommande(texteJson) {
let donnees;
try {
donnees = JSON.parse(texteJson);
} catch {
throw new Error("JSON invalide");
}
if (donnees === null || typeof donnees !== "object") {
throw new Error("objet attendu");
}
const { article, quantite } = donnees;
if (typeof article !== "string" || article.length === 0 || article.length > 50) {
throw new Error("article invalide");
}
if (!Number.isInteger(quantite) || quantite < 1 || quantite > 100) {
throw new Error("quantité invalide");
}
return { article, quantite }; // on ne renvoie que ce qu'on a validé
}
console.log(lireCommande('{"article":"stylo","quantite":3,"inconnu":"ignoré"}'));- Résultat attendu :
{ article: 'stylo', quantite: 3 }. La technique : liste blanche des champs et des valeurs.
À retenir : la validation côté navigateur améliore l'expérience mais ne protège rien : un client malveillant la contourne. La validation qui compte est celle du serveur. Les types TypeScript ne valident pas non plus les données reçues à l'exécution (chapitre 13).
Autres réflexes : pas de secret dans le code côté navigateur (tout est lisible) ; politique de sécurité de contenu (CSP) ; ne pas stocker de jeton sensible dans localStorage (lisible par tout script de la page, y compris injecté) ; npm audit et verrou (chapitre 11).
13. TypeScript : types, interfaces et génériques
TypeScript ajoute des annotations de type (: type) et vérifie le programme avant l'exécution. D'après la documentation, l'inférence déduit souvent le type d'après l'initialisation (let nom = "Alice" est de type string) : n'annoter que ce qui n'est pas évident (paramètres, retours exportés). Les types sont effacés : une assertion de type (as) n'a aucun effet à l'exécution, donc ne vérifie rien.
type Identifiant = string | number; // union
type Statut = "ouvert" | "ferme" | "archive"; // types littéraux
interface Ticket {
id: Identifiant;
titre: string;
statut: Statut;
priorite?: number; // propriété facultative
readonly creeLe: string; // lecture seule
}
function resume(t: Ticket): string {
return `${t.id} — ${t.titre} (${t.statut})`;
}
const t: Ticket = { id: 7, titre: "Exemple fictif", statut: "ouvert", creeLe: "2026-01-01" };
console.log(resume(t));
// resume({ id: 8, titre: "x", statut: "perdu", creeLe: "" }); // erreur : "perdu" n'est pas un Statut- Résultat attendu :
7 — Exemple fictif (ouvert); la ligne commentée serait refusée par le compilateur.
type ou interface ? La page « Everyday Types » résume : l'interface s'étend (extends) et peut être rouverte pour ajouter des champs ; l'alias de type ne le peut pas mais exprime unions et primitifs ; les intersections (&) d'alias peuvent coûter plus cher que extends. Règle pratique : interface pour la forme d'un objet, type pour unions et combinaisons.
any, unknown, never : any désactive la vérification (à éviter) ; unknown est le type sûr d'une valeur inconnue, qu'il faut réduire avant usage (chapitre 14) ; never est le type de ce qui ne peut pas arriver (exhaustivité). Pour une donnée externe, typer unknown puis valider (chapitre 12).
Assertions : x as Type dit au compilateur « je sais mieux que toi » ; l'opérateur ! (non-null) retire null et undefined sans test. La documentation précise de n'utiliser ! que si l'on est sûr. À éviter par défaut.
Génériques
Un générique paramètre un type, pour écrire du code réutilisable qui garde la sécurité de typage. Une contrainte (extends) limite les types acceptés.
function premier<T>(valeurs: T[]): T | undefined {
return valeurs[0];
}
function lirePropriete<T, K extends keyof T>(objet: T, cle: K): T[K] {
return objet[cle];
}
interface Resultat<T> {
ok: boolean;
valeur?: T;
erreur?: string;
}
const nombre = premier([3, 4, 5]); // type : number | undefined
const titre = lirePropriete(t, "titre"); // type : string
// lirePropriete(t, "inconnu"); // erreur : "inconnu" n'est pas une clé de Ticket
const r: Resultat<number> = { ok: true, valeur: 12 };
console.log(nombre, titre, r);- Résultat attendu :
3 Exemple fictif { ok: true, valeur: 12 }(en supposanttdéfini comme ci-dessus).
Typage des fonctions et tableaux : (a: number, b: number) => number, string[] ou Array<string>, Record<string, number> pour un dictionnaire (Record : type utilitaire courant, non relu pour ce cours).
Attention : TypeScript ne contrôle que ce qu'il voit à la compilation. JSON.parse(texte) as Ticket compile et ne garantit rien. Les types décrivent une intention ; la validation d'exécution reste nécessaire pour toute entrée externe.
14. Unions et réduction de type (narrowing)
Une valeur de type union (string | number) n'offre que ce que tous les membres ont en commun. Le compilateur réduit (narrow) le type selon les tests du code (analyse du flot de contrôle). La page « Narrowing » liste :
| Technique | Exemple |
|---|---|
typeof | typeof x === "string" |
| vérité (truthiness) | if (x) retire null, undefined, 0, "" |
| égalité | x === y ; x == null couvre null et undefined |
in | "nage" in animal |
instanceof | x instanceof Date |
| fonction « type predicate » | function estPoisson(a): a is Poisson |
| union discriminée | un champ littéral commun, souvent kind |
| exhaustivité | affecter la valeur restante à never dans le default |
interface Cercle { genre: "cercle"; rayon: number }
interface Carre { genre: "carre"; cote: number }
type Forme = Cercle | Carre;
function aire(f: Forme): number {
switch (f.genre) {
case "cercle":
return Math.PI * f.rayon ** 2;
case "carre":
return f.cote ** 2;
default: {
const verifie: never = f; // erreur de compilation si on ajoute une forme sans la traiter
return verifie;
}
}
}
function afficher(valeur: unknown): string {
if (typeof valeur === "string") return valeur.toUpperCase();
if (typeof valeur === "number") return valeur.toFixed(2);
if (valeur instanceof Date) return valeur.toISOString();
return "type non géré";
}
console.log(aire({ genre: "carre", cote: 3 }), afficher(3.14159), afficher("abc"));- Résultat attendu :
9 3.14 ABC.
Astuce : modéliser un résultat qui peut échouer par une union discriminée ({ ok: true, valeur } | { ok: false, erreur }) plutôt que par des champs facultatifs : le compilateur oblige alors à tester ok avant d'accéder à valeur.
15. tsconfig, strict et outils
Le fichier tsconfig.json configure le compilateur. D'après la référence tsconfig, l'option strict (activée dans la plupart des projets récents, recommandée) active la famille : alwaysStrict, strictNullChecks, strictBindCallApply, strictBuiltinIteratorReturn, strictFunctionTypes, strictPropertyInitialization, noImplicitAny, noImplicitThis et useUnknownInCatchVariables. La page précise que des versions futures peuvent y ajouter d'autres contrôles.
| Option | Effet |
|---|---|
strictNullChecks | null et undefined deviennent des types à gérer explicitement |
noImplicitAny | refuse un type any implicite quand rien ne peut être inféré |
noUncheckedIndexedAccess | ajoute undefined aux accès par index (disponible depuis TypeScript 4.1) ; non incluse dans strict |
target, module, lib | version de JavaScript produite, système de modules, API connues du compilateur |
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"strict": true,
"noUncheckedIndexedAccess": true,
"outDir": "dist"
},
"include": ["src"]
}À retenir : les valeurs de target et module ci-dessus sont des exemples non vérifiés pour la version de TypeScript et de Node.js de Stéphane (versions à confirmer) ; les valeurs possibles dépendent de la version du compilateur. Partir de strict: true dès le début : l'ajouter plus tard à un gros projet coûte cher.
npx tsc --noEmitBloc actif : la touche C copie la commande seule, sans invite ni sortie.
- Contexte : Terminal, dossier de projet de test avec
tsconfig.json; TypeScript installé dans le projet (installation non décrite ici ; version non vérifiée) ;npxexécute le compilateur local. Si TypeScript n'est pas installé,npxpeut proposer de le télécharger : lire le message avant d'accepter. - Droits : utilisateur normal ; lecture des fichiers du projet.
- Effet : vérifie les types sans produire de fichier (
--noEmit, option du compilateur non relue dans la pagetsconfiglue). - Vérification : silence et code
0si aucune erreur ; sinon des messagesfichier(ligne,colonne) : error TS…. - Retour arrière : sans objet (aucune écriture).
Attention : exécuter directement des fichiers .ts avec Node.js (retrait des types intégré à certaines versions) est une fonction dont l'état dépend de la version : non vérifié ici ; consulter la documentation Node.js de la version installée avant de s'y appuyer. Le retrait des types ne vérifie pas les types.
16. Pièges courants
| Piège | Pourquoi | Remède |
|---|---|---|
== au lieu de === | coercions surprenantes ("0" == false) | === partout |
0.1 + 0.2 !== 0.3 | nombres flottants | comparer avec une tolérance, ou compter en centimes entiers |
typeof null === "object" | particularité historique | tester x === null |
NaN !== NaN | par définition | Number.isNaN(x) |
sort() sans comparateur | tri par chaînes | sort((a, b) => a - b) |
this perdu | méthode passée en rappel | flèche, bind, ou appel via l'objet |
await oublié | on manipule une promesse | await, et une règle de lint |
await dans forEach | forEach n'attend pas | for…of ou Promise.all |
| rejet non géré | processus arrêté ou erreur silencieuse | catch à chaque chaîne de promesses |
| modification d'un tableau en place | sort, reverse, splice | copier avant ([...a]) |
| copie superficielle | {...o} partage l'imbriqué | structuredClone ou copie ciblée |
parseInt("08") sans base | habitude | parseInt(x, 10) ou Number(x) |
for…in sur un tableau | parcourt les clés, pas les valeurs | for…of |
var en boucle + rappel | portée de fonction | let |
as et ! en TypeScript | font taire le compilateur | validation d'exécution, réduction de type |
npm install sur un projet inconnu | scripts d'installation | --ignore-scripts, dossier jetable, lecture préalable |
Exercices et vérifications de fin de cours
Tous dans un dossier de test jetable, sur des données fictives. Node.js suffit pour les exercices 1 à 8 ; le navigateur pour l'exercice 9 ; le compilateur TypeScript (installé par vos soins, version à confirmer) pour les exercices 11 à 12. Ces exercices ne sont pas exécutés par le générateur.
- Coercition. Écrire
coercion.mjsqui affiche, pour dix paires de valeurs de votre choix,a == b,a === betObject.is(a, b); expliquer chaque différence. - Closure. Écrire
creerCompteur(chapitre 3) avec un pas paramétrable ; vérifier que deux compteurs sont indépendants. - this. Détacher la méthode
direBonjour, provoquer l'erreur, puis la corriger de deux façons (bind, flèche). - Tableaux. Avec ce jeu fictif :
const ventes = [{ pays: "FR", montant: 12 }, { pays: "BE", montant: 7 }, { pays: "FR", montant: 5 }];, calculer le total par pays avecreduce, puis trier les pays par total décroissant sans modifierventes. - Modules. Découper un petit programme en
maths.mjsetprincipal.mjs(chapitre 6) ; ajouter un import dynamique. - Boucle d'événements. Prévoir par écrit l'ordre d'affichage de 6
console.logmêlant synchrone,setTimeout(…, 0),Promise.resolve().thenetqueueMicrotask, puis lancer le programme et comparer. - Asynchrone. Réécrire en
async/awaitune suite de trois fonctions à rappels simulées parsetTimeout; ajouterPromise.allpuisPromise.allSettledet expliquer la différence quand l'une échoue. - JSON et validation. Reprendre
lireCommande(chapitre 12), lui soumettre cinq entrées dont trois invalides (JSON cassé, quantité négative, article de 80 caractères) et relever chaque message. - DOM. Enregistrer la page du chapitre 9 sous
essai.html, l'ouvrir dans le navigateur ; saisir<b>test</b>et constater qu'il s'affiche comme texte ; remplacertextContentparinnerHTMLdans cette page de test seulement, refaire l'essai, puis revenir àtextContent. - Node.js. Écrire
salutations.mjs(chapitre 10) ; vérifier les deux codes de sortie. - TypeScript. Typer l'interface
Ticketet la fonctionresume(chapitre 13) ; provoquer volontairement trois erreurs de type et les lire (npx tsc --noEmit). - Union discriminée. Ajouter une forme
TriangleàForme; constater queairene compile plus tant que le cas n'est pas traité, puis le traiter. - npm (lecture seule). Dans un dossier jetable, créer un
package.jsonminimal, lancernpm lspuisnpm audit(sans installation) et expliquer le message ; citer les trois scripts de cycle de vie qui s'exécutent à l'installation d'un paquet. - Sécurité. Citer trois constructions interdites avec une donnée externe (
eval,innerHTML,execavec une chaîne) et le remplaçant de chacune.
Critères de réussite
- Prévoir le résultat de
"0" == 0,"5" + 1,[10, 2, 1].sort()et dire pourquoi. - Expliquer une closure et la valeur de
thisselon l'appel. - Ordonner synchrone, micro-tâches et tâches, et justifier avec le schéma de la boucle d'événements.
- Écrire une fonction
asyncavec gestion d'erreur et unPromise.all. - Dire en une phrase pourquoi
innerHTMLavec une entrée non fiable est dangereux, et ce qui le remplace. - Expliquer la différence entre
npm installetnpm ci, et le risque des scripts d'installation. - Réduire un type union et utiliser une union discriminée avec exhaustivité.
- Expliquer ce qu'ajoute
strictet pourquoi les types ne valident pas les données reçues. - Citer le périmètre des sources (documentation en ligne du 17 octobre 2026) sans la confondre avec les versions installées.
Points non vérifiés ou à confirmer
- Versions de Node.js, npm, TypeScript et du navigateur sur le Mac de Stéphane : inconnues, aucune n'est choisie. Les numéros cités (Node.js v26.12.0, npm 11.21.0, brouillon ECMAScript 2027, TypeScript 5.8 cité par la référence
tsconfig) sont ceux de la documentation consultée. Statut des lignes Node.js (« Current », « Active LTS », « Maintenance LTS ») : lu sur la page des versions, la page elle-même présentait une incohérence apparente de date pour v26 : à reconfronter. - Pages lues par un outil de synthèse automatique : citations et exemples en traduction libre, à relire sur les pages. MDN : Event loop, Promise,
this,innerHTML, Equality comparisons,import. Node.js : ESM,process, versions. npm :audit,ci, scripts. TypeScript :tsconfig, Narrowing, Everyday Types, Generics. tc39.es : page d'accueil seulement. - Faits de langage donnés d'après la connaissance générale, non relus sur MDN ou la spécification pour ce cours : portée de
var,typeof null, coercition de+et-,sortpar chaînes, méthodes de tableau (map,filter,reduce,find,some,every) et leur non-mutation, chaîne de prototypes, champs privés#,JSON.parseetSyntaxError,structuredClone,for…in,parseInt, pollution de prototype. - Détails de Node.js non relus :
child_process.exec/execFile/spawn,node --test, comportement d'un rejet non géré selon la version, retrait natif des types TypeScript, état du modèle de permissions (pageprocess: API marquée expérimentale). - npm : effet exact de
--ignore-scriptssur chaque commande ; cycle de vie complet ; typosquatting (risque général, non documenté dans les pages lues) ; nombre moyen de dépendances (ordre de grandeur non sourcé) ; commandesnpm approve-scripts/npm deny-scriptsaperçues dans la page des scripts, non détaillées ni décrites ici. - TypeScript : valeurs de
targetetmodulede l'exempletsconfig.json; option--noEmit;Record;npx tscdans un projet où TypeScript n'est pas installé. - Prise en charge de
Element.setHTML(), de l'API Sanitizer et de Trusted Types selon les navigateurs : non vérifiée. - Exemples de code : écrits pour ce cours et jamais exécutés (consigne) ; résultats annoncés déduits par lecture, à confirmer à l'exécution.
Sources officielles
Toutes consultées le 17 octobre 2026. Versions affichées : Node.js v26.12.0, npm v11.21.0, ECMAScript 2027 (brouillon) ; MDN : date de dernière modification indiquée entre parenthèses ; TypeScript : numéro non affiché (état de la page à la date de consultation).