Aller au contenu
Sommaire

Cours JavaScript et TypeScript — le langage, l'asynchrone, npm, la sécurité et le typage

Cours · environ 38 min de lecture

Validé

Validé. Ce site n’exécute aucune commande. 8 points non vérifiés · Sources

Objectifs d'apprentissage

À la fin de ce cours, l'apprenant peut :

  1. déclarer des variables avec const et let, et prévoir le résultat d'une coercition ou d'une comparaison ;
  2. écrire des fonctions (déclaration, flèche), expliquer la portée, les fermetures (closures) et la valeur de this ;
  3. manipuler objets, classes et tableaux (map, filter, reduce), avec destructuration et spread ;
  4. organiser du code en modules ES (import / export) ;
  5. expliquer la boucle d'événements et ordonner pile, micro-tâches et tâches ;
  6. écrire du code asynchrone avec promesses et async / await, et gérer les erreurs ;
  7. lire et produire du JSON sans exécuter de données ;
  8. distinguer l'exécution dans le navigateur de celle dans Node.js ;
  9. utiliser npm avec méthode : fichier de verrouillage, npm ci, audit, scripts d'installation ;
  10. reconnaître et éviter les failles courantes : XSS, innerHTML, eval, entrée non fiable ;
  11. typer un programme TypeScript : types, interfaces, génériques, unions et réduction de type, strict ;
  12. é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).

TermeSens
ECMAScriptla norme du langage (syntaxe, objets de base)
JavaScriptle 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)
TypeScriptJavaScript 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).

javascript
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.

javascript
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, puis false 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.

ExpressionRésultatCause
"0" == 0truela chaîne devient un nombre
0 == falsetruele booléen devient un nombre
"0" === 0falsetypes différents
null == undefinedtruerègle particulière de ==
NaN === NaNfalseseul 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" - 14- 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 ||.

javascript
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, puis 0, puis 12 NaN 12.

3. Fonctions, portée, closures et this

Trois écritures

javascript
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.

javascript
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 de creerCompteur() crée un n distinct.

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 :

SituationValeur de this
appel de méthode obj.f()obj
appel simple f()undefined en mode strict, objet global sinon
fonction flèchecelui de la portée englobante, figé ; bind, call, apply n'y changent rien
new F()le nouvel objet créé
méthode de classel'instance (méthode statique : la classe)
bind(obj)nouvelle fonction avec this fixé
gestionnaire d'événement DOMl'élément sur lequel l'écouteur est attaché
haut niveau d'un moduleundefined
javascript
"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, this vaut undefined et un TypeError est 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).

javascript
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 : cri est trouvée par le prototype, elle n'appartient pas à chien.
javascript
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.#solde hors 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.

javascript
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 (stylo 6, cahier 5), 11, puis 5, puis true 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

javascript
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.

javascript
// 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;
}
javascript
// 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, puis 1.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), queueMicrotask et les MutationObserver ;
  • 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).
Schéma : une pile d'appels envoie des appels asynchrones à l'API de l'hôte (minuteurs, réseau, DOM) ; quand un résultat est prêt, son rappel entre dans la file de micro-tâches (promesses, await) ou dans la file de tâches (minuteurs, événements) ; quand la pile est vide, la boucle d'événements vide toutes les micro-tâches puis prend une tâche.1. Pile d'appelsle code en cours, un appel à la foissetTimeout, fetch…2. API de l'hôtenavigateur ou Node.js : minuteurs, réseau, DOMrésultat prêt3a. File de micro-tâchesthen, await, queueMicrotask3b. File de tâchesminuteurs, événements, entrées-sortiespile vide4. Boucle d'événementstoutes les micro-tâches, puis une tâcheChaque travail va au bout avant le suivant
La boucle d'événements : pile d'appels, API de l'hôte, micro-tâches et tâches. Schéma pédagogique d'après le modèle d'exécution décrit par MDN, simplifié (le rendu du navigateur et les détails de Node.js ne sont pas représentés).
javascript
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, 4 dans 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éthodeComportement
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.

javascript
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.

javascript
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.

javascript
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ù date est une chaîne et absent a disparu, puis capté : 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.

html
<!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

SujetNavigateurNode.js (v26.12.0 dans la documentation lue)
Objet globalwindow (globalThis)globalThis ; pas de window ni de document
Page, DOMouinon
Fichierspas d'accès libre au disquemodule node:fs
Arguments, environnementsans objetprocess.argv, process.env
Code de sortiesans objetprocess.exitCode
Modules<script type="module">.mjs, "type", CommonJS (require)
Sécuritébac à sable du navigateur, politique de même originele 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).

javascript
// 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}`);
}
Terminal
node salutations.mjs Alicenode salutations.mjsecho $?
  • 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 sur stderr et positionne le code de sortie.
  • Vérification : echo $? affiche 1 après le second appel (et 0 aprè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.

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.

RisqueExplicationParade
Scripts d'installationnpm 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 suivantefichier 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 compromiseun mainteneur ou un compte est piratéverrou, audit, vérification des signatures
Vulnérabilité connuefaille publiée dans une dépendancenpm 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.

Terminal
npm install --ignore-scripts nom-du-paquet
  • Contexte : Terminal, dossier de test contenant un package.json ; npm installé (version non vérifiée) ; accès réseau au registre ; nom-du-paquet est 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 à jour package.json et package-lock.json ; --ignore-scripts empêche l'exécution des scripts d'installation (l'option existe, d'après la page npm ci ; son effet exact sur chaque commande n'a pas été relu).
  • Vérification : npm ls liste l'arbre installé ; relire le diff de package-lock.json.
  • Retour arrière : npm uninstall nom-du-paquet, ou supprimer node_modules et restaurer package.json et package-lock.json (idéalement versionnés avec git).
Terminal
npm cinpm audit
  • 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 ci installe exactement ce que fige le verrou (et exécute les scripts d'installation des dépendances, sauf --ignore-scripts) ; npm audit envoie les données de dépendances au registre et affiche un rapport, sans rien modifier.
  • Vérification : npm ci se termine sans erreur de désynchronisation ; echo $? après npm audit donne 0 ou 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 en root ;
  • lancer npm audit ré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é).

javascript
// 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) et new 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 un Map.
  • Chaînes de commande : `child_process.exec(ls ${entree}) passe par un shell et permet l'injection de commande (même principe que subprocess avec shell=True en Python). Préférer execFile ou spawn` 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 href ou location (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 utiliser Object.create(null) / Map pour des clés venant de l'extérieur (risque connu, non relu dans les sources officielles consultées).

Valider une entrée

javascript
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.

typescript
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.

typescript
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 supposant t dé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 :

TechniqueExemple
typeoftypeof x === "string"
vérité (truthiness)if (x) retire null, undefined, 0, ""
égalitéx === y ; x == null couvre null et undefined
in"nage" in animal
instanceofx instanceof Date
fonction « type predicate »function estPoisson(a): a is Poisson
union discriminéeun champ littéral commun, souvent kind
exhaustivitéaffecter la valeur restante à never dans le default
typescript
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.

OptionEffet
strictNullChecksnull et undefined deviennent des types à gérer explicitement
noImplicitAnyrefuse un type any implicite quand rien ne peut être inféré
noUncheckedIndexedAccessajoute undefined aux accès par index (disponible depuis TypeScript 4.1) ; non incluse dans strict
target, module, libversion de JavaScript produite, système de modules, API connues du compilateur
JSON
{
  "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.

Terminal
npx tsc --noEmit
  • 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) ; npx exécute le compilateur local. Si TypeScript n'est pas installé, npx peut 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 page tsconfig lue).
  • Vérification : silence et code 0 si aucune erreur ; sinon des messages fichier(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ègePourquoiRemède
== au lieu de ===coercions surprenantes ("0" == false)=== partout
0.1 + 0.2 !== 0.3nombres flottantscomparer avec une tolérance, ou compter en centimes entiers
typeof null === "object"particularité historiquetester x === null
NaN !== NaNpar définitionNumber.isNaN(x)
sort() sans comparateurtri par chaînessort((a, b) => a - b)
this perduméthode passée en rappelflèche, bind, ou appel via l'objet
await oubliéon manipule une promesseawait, et une règle de lint
await dans forEachforEach n'attend pasfor…of ou Promise.all
rejet non géréprocessus arrêté ou erreur silencieusecatch à chaque chaîne de promesses
modification d'un tableau en placesort, reverse, splicecopier avant ([...a])
copie superficielle{...o} partage l'imbriquéstructuredClone ou copie ciblée
parseInt("08") sans basehabitudeparseInt(x, 10) ou Number(x)
for…in sur un tableauparcourt les clés, pas les valeursfor…of
var en boucle + rappelportée de fonctionlet
as et ! en TypeScriptfont taire le compilateurvalidation d'exécution, réduction de type
npm install sur un projet inconnuscripts 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.

  1. Coercition. Écrire coercion.mjs qui affiche, pour dix paires de valeurs de votre choix, a == b, a === b et Object.is(a, b) ; expliquer chaque différence.
  2. Closure. Écrire creerCompteur (chapitre 3) avec un pas paramétrable ; vérifier que deux compteurs sont indépendants.
  3. this. Détacher la méthode direBonjour, provoquer l'erreur, puis la corriger de deux façons (bind, flèche).
  4. Tableaux. Avec ce jeu fictif : const ventes = [{ pays: "FR", montant: 12 }, { pays: "BE", montant: 7 }, { pays: "FR", montant: 5 }];, calculer le total par pays avec reduce, puis trier les pays par total décroissant sans modifier ventes.
  5. Modules. Découper un petit programme en maths.mjs et principal.mjs (chapitre 6) ; ajouter un import dynamique.
  6. Boucle d'événements. Prévoir par écrit l'ordre d'affichage de 6 console.log mêlant synchrone, setTimeout(…, 0), Promise.resolve().then et queueMicrotask, puis lancer le programme et comparer.
  7. Asynchrone. Réécrire en async / await une suite de trois fonctions à rappels simulées par setTimeout ; ajouter Promise.all puis Promise.allSettled et expliquer la différence quand l'une échoue.
  8. 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.
  9. 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 ; remplacer textContent par innerHTML dans cette page de test seulement, refaire l'essai, puis revenir à textContent.
  10. Node.js. Écrire salutations.mjs (chapitre 10) ; vérifier les deux codes de sortie.
  11. TypeScript. Typer l'interface Ticket et la fonction resume (chapitre 13) ; provoquer volontairement trois erreurs de type et les lire (npx tsc --noEmit).
  12. Union discriminée. Ajouter une forme Triangle à Forme ; constater que aire ne compile plus tant que le cas n'est pas traité, puis le traiter.
  13. npm (lecture seule). Dans un dossier jetable, créer un package.json minimal, lancer npm ls puis npm audit (sans installation) et expliquer le message ; citer les trois scripts de cycle de vie qui s'exécutent à l'installation d'un paquet.
  14. Sécurité. Citer trois constructions interdites avec une donnée externe (eval, innerHTML, exec avec 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 this selon 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 async avec gestion d'erreur et un Promise.all.
  • Dire en une phrase pourquoi innerHTML avec une entrée non fiable est dangereux, et ce qui le remplace.
  • Expliquer la différence entre npm install et npm 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 strict et 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 -, sort par chaînes, méthodes de tableau (map, filter, reduce, find, some, every) et leur non-mutation, chaîne de prototypes, champs privés #, JSON.parse et SyntaxError, 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 (page process : API marquée expérimentale).
  • npm : effet exact de --ignore-scripts sur 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é) ; commandes npm approve-scripts / npm deny-scripts aperçues dans la page des scripts, non détaillées ni décrites ici.
  • TypeScript : valeurs de target et module de l'exemple tsconfig.json ; option --noEmit ; Record ; npx tsc dans 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).