2.3 · Tableaux d’objets : map, filter, find, reduce

Les diapositives de la séance 2

  • parcourir un tableau d’objets sans écrire de boucle for
  • choisir entre forEach, map, filter, find et reduce selon le résultat voulu
  • suivre pas à pas l’accumulateur d’un reduce
  • lire une chaîne de méthodes étape par étape, en connaissant le type à chaque étape
  • éviter les pièges classiques : undefined de find, reduce sans valeur initiale, map qui ne renvoie rien

Les fonctions fléchées et le passage de fonction en paramètre : 2.1 · Fonctions fléchées. Les objets, les interfaces et la copie par décomposition : 2.2 · Objets de données.

Le code de ce chapitre : télécharger app.ts. Exécutez-le avec npx tsx app.ts.

Décrire au lieu de dérouler

En C, parcourir un tableau impose de gérer soi-même l’indice, la borne et le tableau de destination :

for (int i = 0; i < n; i++) { noms[i] = produits[i].nom; }

Trois occasions de se tromper (i mal initialisé, borne fausse, taille de la destination) pour une idée très simple : « le nom de chaque produit ». En TypeScript, cette idée s’écrit telle quelle :

const noms = produits.map((p) => p.nom);

Toutes les méthodes de ce chapitre suivent le même principe : vous fournissez une fonction, le tableau se charge du parcours. Reste à choisir la bonne méthode, et le critère de choix est toujours le même : qu’est-ce que je veux obtenir à la fin ?

Je veux… Méthode Ce qui est renvoyé
faire quelque chose pour chaque élément forEach void : rien
transformer chaque élément map un nouveau tableau, même longueur
ne garder que certains éléments filter un nouveau tableau, plus court ou égal
trouver le premier élément qui convient find un élément, ou undefined
réduire tout le tableau à une seule valeur reduce une valeur, du type que vous voulez

Le tableau de départ n’est jamais modifié par map, filter, find ni reduce.

Tous les exemples partent de ce catalogue, un tableau d’objets comme ceux du chapitre 2.2 :

app.ts
interface Produit {
  nom: string;
  categorie: string;
  prix: number;
  stock: number;
}

const produits: Produit[] = [
  { nom: "Clavier", categorie: "informatique", prix: 45, stock: 12 },
  { nom: "Souris", categorie: "informatique", prix: 20, stock: 0 },
  { nom: "Cahier", categorie: "papeterie", prix: 3, stock: 150 },
  { nom: "Stylo", categorie: "papeterie", prix: 1.5, stock: 300 },
  { nom: "Écran", categorie: "informatique", prix: 180, stock: 4 },
  { nom: "Agenda", categorie: "papeterie", prix: 12, stock: 0 },
];

forEach : agir sur chaque élément

Intuition : un for sans indice à gérer.

forEach(callback: (element, index, tableau) => void): void
app.ts
produits.forEach((p) => {
  console.log(`${p.nom} : ${p.prix} €`);
});
// une ligne par produit : Clavier : 45 €, Souris : 20 €, ...

forEach renvoie void. On l’utilise pour ses effets : afficher, insérer un élément dans la page, enregistrer quelque chose.

map : transformer

Intuition : une machine qui reçoit un tableau de n éléments et en rend un autre de n éléments, un pour un.

map(callback: (element, index) => resultat): resultat[]
app.ts
const noms: string[] = produits.map((p) => p.nom);
console.log(noms);   // ["Clavier", "Souris", "Cahier", "Stylo", "Écran", "Agenda"]

map peut changer de type au passage : ici, un tableau de Produit devient un tableau de string. Il peut aussi produire de nouveaux objets, avec la décomposition du chapitre 2.2 :

app.ts
const soldes: Produit[] = produits.map((p) => ({ ...p, prix: p.prix / 2 }));
console.log(soldes[0].prix, produits[0].prix);   // 22.5 45 : l'original est intact

Les parenthèses autour de { ...p, prix: … } sont indispensables : sans elles, les accolades seraient lues comme un bloc d’instructions (voir 2.1).

produits.map((p) => ({ ...p, prix: p.prix / 2 }));
console.log(produits[0].prix);   // 45 : inchangé

map ne modifie pas le tableau d’origine, il en construit un nouveau. Si vous n’affectez pas le résultat à une variable, le travail est perdu. Une ligne map isolée, sans const à gauche, est presque toujours un bug.

Quand l’utiliser : dès que vous voulez « la même liste, mais autrement ».

filter : sélectionner

Intuition : un tamis. Chaque élément est soumis à un test ; il passe ou il ne passe pas. Les éléments retenus sont conservés tels quels.

filter(predicat: (element) => boolean): element[]
app.ts
const enStock: Produit[] = produits.filter((p) => p.stock > 0);
console.log(enStock.length);   // 4 : Clavier, Cahier, Stylo, Écran

La fonction passée à filter s’appelle un prédicat : elle doit renvoyer un booléen. map transforme, filter trie ; ne mélangez pas les deux rôles.

Quand l’utiliser : « je veux la même liste, en plus court ».

find et findIndex : chercher

Intuition : filter qui s’arrête au premier succès.

find(predicat: (element) => boolean): element | undefined
app.ts
const ecran: Produit | undefined = produits.find((p) => p.nom === "Écran");
if (ecran !== undefined) {
  console.log(`Écran : ${ecran.prix} €`);   // Écran : 180 €
}

const position: number = produits.findIndex((p) => p.stock === 0);
console.log(position);   // 1 : la souris est le premier produit épuisé

Le type de ecran est Produit | undefined : si aucun produit ne s’appelle « Écran », find renvoie undefined. Le if n’est pas une prudence décorative, c’est ce que le compilateur exige avant de vous laisser écrire ecran.prix.

Quand l’utiliser : chercher un utilisateur par son identifiant, un produit par son nom. Sur un grand tableau, find est aussi plus rapide que filter, puisqu’il s’arrête dès qu’il a trouvé.

reduce : tout ramener à une valeur

C’est la plus puissante et la moins intuitive. Intuition : vous parcourez le tableau avec un accumulateur dans la main. À chaque élément, vous décidez de la nouvelle valeur de cet accumulateur. À la fin, vous rendez ce que vous avez dans la main.

reduce(reducteur: (acc, element) => acc, valeurInitiale): acc

La valeur totale du stock, c’est-à-dire la somme des prix × stock :

app.ts
const valeurStock: number = produits.reduce((acc, p) => acc + p.prix * p.stock, 0);
console.log(valeurStock);   // 2160

Le 0 est la valeur de départ de l’accumulateur. Déroulons :

Étape acc avant p p.prix * p.stock acc après
départ 0
1 0 Clavier 45 × 12 = 540 540
2 540 Souris 20 × 0 = 0 540
3 540 Cahier 3 × 150 = 450 990
4 990 Stylo 1,5 × 300 = 450 1440
5 1440 Écran 180 × 4 = 720 2160
6 2160 Agenda 12 × 0 = 0 2160

L’accumulateur n’est pas forcément un nombre : c’est ce qui rend reduce si général. Ici il contient « le produit le plus cher vu jusqu’ici », et la valeur initiale est le premier produit :

app.ts
const plusCher: Produit = produits.reduce(
  (meilleur, p) => (p.prix > meilleur.prix ? p : meilleur),
  produits[0]
);
console.log(plusCher.nom);   // Écran

Quand l’utiliser : somme, moyenne, maximum, concaténation, comptage par catégorie : tout ce qui condense une liste en un seul résultat.

AstuceQuelle méthode ?

Posez-vous une seule question : qu’est-ce que je veux obtenir à la fin ?

Je veux obtenir…
├── rien : j'agis sur chaque élément (afficher, insérer dans la page)  →  forEach
├── un tableau
│   ├── de même longueur, chaque élément transformé                    →  map
│   └── plus court, avec les éléments intacts                          →  filter
└── une seule valeur
    ├── un élément du tableau : le premier qui convient                →  find
    └── un résumé : somme, maximum, texte, comptage                    →  reduce

Transformer, c’est map ; garder, c’est filter ; un seul, c’est find ; résumer, c’est reduce.

Enchaîner les opérations

filter et map renvoient un tableau, sur lequel on peut appeler la méthode suivante. On lit alors le traitement de haut en bas, comme une phrase :

app.ts
const valeurInformatique: number = produits
  .filter((p) => p.categorie === "informatique")   // Clavier, Souris, Écran
  .map((p) => p.prix * p.stock)                    // [540, 0, 720]
  .reduce((acc, v) => acc + v, 0);                 // 1260
console.log(valeurInformatique);                   // 1260

Filtrez le plus tôt possible : les étapes suivantes travaillent alors sur moins d’éléments. Et n’enchaînez pas indéfiniment : au-delà de trois ou quatre maillons, nommez des étapes intermédiaires dans des constantes.

Lire une chaîne de méthodes

Une chaîne se lit maillon par maillon : chaque méthode reçoit le résultat de la précédente. Pour la comprendre, écrivez à chaque étape ce qui entre, ce qui sort, et son type.

app.ts
const resume: string = produits
  .filter((p) => p.categorie === "papeterie")
  .filter((p) => p.stock > 0)
  .map((p) => `${p.nom} (${p.prix} €)`)
  .join(", ");
Étape Reçoit Renvoie Type de la sortie
produits les 6 produits Produit[]
.filter(p => p.categorie === "papeterie") 6 produits Cahier, Stylo, Agenda Produit[]
.filter(p => p.stock > 0) 3 produits Cahier, Stylo Produit[]
.map(p => ...) 2 produits ["Cahier (3 €)", "Stylo (1.5 €)"] string[]
.join(", ") 2 chaînes "Cahier (3 €), Stylo (1.5 €)" string

Deux réflexes rendent n’importe quelle chaîne lisible :

  • suivre le type : tant qu’il reste Produit[], on peut encore filtrer ou lire p.prix ; après un map vers string[], les propriétés du produit ne sont plus accessibles ; après reduce ou join, ce n’est plus un tableau du tout ;
  • couper pour vérifier : quand le résultat surprend, rangez chaque étape dans une constante et affichez-la. Survoler une constante dans VS Code affiche aussi son type.

C’est exactement ce que demandent les questions « que vaut… » des évaluations.

Les pièges

find peut renvoyer undefined. Quand rien ne correspond, find renvoie undefined et findIndex renvoie -1. Le type est Produit | undefined, et TypeScript refuse tablette.prix tant que le cas n’est pas traité :

const tablette = produits.find((p) => p.nom === "Tablette");
console.log(tablette.prix);   // ERREUR : 'tablette' is possibly 'undefined'

console.log(tablette === undefined ? "Pas de tablette" : tablette.prix);   // correct

reduce sans valeur initiale. Sans le 0 final, le premier élément sert d’accumulateur de départ. Sur un tableau d’objets, l’accumulateur devient donc un Produit, et le calcul n’a plus de sens :

produits.reduce((acc, p) => acc + p.prix);
// ERREUR : Operator '+' cannot be applied to types 'Produit' and 'number'.

Sur un tableau vide, cette version lève en plus TypeError: Reduce of empty array with no initial value. Fournissez toujours la valeur initiale : elle fixe le type de l’accumulateur. Et avec un corps à accolades, return acc; est obligatoire, sinon l’accumulateur vaut undefined dès le deuxième tour.

map qui ne renvoie rien. Avec des accolades, il faut un return :

const doubles: number[] = produits.map((p) => { p.prix * 2; });
// ERREUR : Type 'void[]' is not assignable to type 'number[]'.

Même piège, plus sournois, pour renvoyer un objet : (p) => { nom: p.nom } ne renvoie rien, les accolades sont lues comme un bloc. Écrivez (p) => ({ nom: p.nom }).

forEach ne renvoie rien.

const resultat = produits.forEach((p) => p.prix * 2);   // resultat vaut undefined

Si vous voulez récupérer un tableau, c’est map qu’il faut. Cette confusion est l’erreur la plus fréquente du chapitre.

Afficher un tableau d’objets. Un objet converti en texte donne [object Object] :

console.log(`Catalogue : ${produits}`);
// Catalogue : [object Object],[object Object],...

Pour l’affichage, transformez d’abord chaque objet en texte avec map, puis assemblez avec join, qui choisit le séparateur :

console.log(`Catalogue : ${produits.map((p) => p.nom).join(", ")}`);
// Catalogue : Clavier, Souris, Cahier, Stylo, Écran, Agenda

Exercices

Exercice 2.7 : Des fruits, des fruits, des fruits

essentiel

Partez du tableau suivant, que vous ne modifierez jamais :

interface Fruit {
  nom: string;
  prix: number;    // en euros le kilo
  stock: number;   // en kilos
}

const fruits: Fruit[] = [
  { nom: "pomme", prix: 2.5, stock: 40 },
  { nom: "kiwi", prix: 4, stock: 0 },
  { nom: "banane", prix: 1.8, stock: 25 },
  { nom: "cerise", prix: 9, stock: 5 },
  { nom: "orange", prix: 2.2, stock: 0 },
  { nom: "poire", prix: 3, stock: 12 },
  { nom: "fraise", prix: 7.5, stock: 8 },
  { nom: "prune", prix: 3.5, stock: 0 },
  { nom: "ananas", prix: 3.9, stock: 6 },
  { nom: "pêche", prix: 4.2, stock: 15 },
];
  1. map : un tableau des noms en majuscules.
  2. filter : les fruits en stock.
  3. reduce : la valeur totale du stock (prix × stock, pour tous les fruits).
  4. find : le premier fruit à plus de 5 € le kilo. Puis le premier à plus de 20 € : affichez aucun s’il n’y en a pas.
  5. forEach : afficher chaque fruit précédé de son rang, 1. pomme (2.5 €/kg), etc.
  6. Une seule chaîne contenant tous les noms séparés par ", ", d’abord avec map et join, puis avec reduce seul.

Réussite : les résultats sont corrects, fruits est inchangé à la fin, le cas « aucun fruit trouvé » s’affiche proprement, et votre reduce de la question 6 ne laisse pas de virgule qui traîne en début ou en fin de chaîne.

"pomme".toUpperCase(). Pour le forEach, le deuxième paramètre de la fonction de rappel est l’indice.

fruits.ts
interface Fruit {
  nom: string;
  prix: number;
  stock: number;
}

const fruits: Fruit[] = [
  { nom: "pomme", prix: 2.5, stock: 40 },
  { nom: "kiwi", prix: 4, stock: 0 },
  { nom: "banane", prix: 1.8, stock: 25 },
  { nom: "cerise", prix: 9, stock: 5 },
  { nom: "orange", prix: 2.2, stock: 0 },
  { nom: "poire", prix: 3, stock: 12 },
  { nom: "fraise", prix: 7.5, stock: 8 },
  { nom: "prune", prix: 3.5, stock: 0 },
  { nom: "ananas", prix: 3.9, stock: 6 },
  { nom: "pêche", prix: 4.2, stock: 15 },
];

// 1. map : même longueur, chaque élément transformé
const enMajuscules: string[] = fruits.map((f) => f.nom.toUpperCase());
console.log(enMajuscules);   // ["POMME", "KIWI", "BANANE", ..., "PÊCHE"]

// 2. filter : mêmes objets, moins nombreux
const enStock: Fruit[] = fruits.filter((f) => f.stock > 0);
console.log(enStock.length);   // 7

// 3. reduce : tout le tableau ramené à un nombre
const valeur: number = fruits.reduce((acc, f) => acc + f.prix * f.stock, 0);
console.log(valeur);   // 372.4

// 4. find : un seul élément, ou undefined
const cher: Fruit | undefined = fruits.find((f) => f.prix > 5);
console.log(cher !== undefined ? cher.nom : "aucun");        // cerise

const tresCher: Fruit | undefined = fruits.find((f) => f.prix > 20);
console.log(tresCher !== undefined ? tresCher.nom : "aucun");  // aucun

// 5. forEach : aucun résultat, seulement un effet (l'affichage)
fruits.forEach((f, index) => {
  console.log(`${index + 1}. ${f.nom} (${f.prix} €/kg)`);
});
// 1. pomme (2.5 €/kg)
// 2. kiwi (4 €/kg)
// …

// 6. Une seule chaîne : map + join, puis reduce seul
const liste1: string = fruits.map((f) => f.nom).join(", ");
const liste2: string = fruits.reduce(
  (acc, f) => (acc === "" ? f.nom : acc + ", " + f.nom),
  ""
);
console.log(liste1);   // pomme, kiwi, banane, cerise, orange, poire, fraise, prune, ananas, pêche
console.log(liste1 === liste2);   // true

console.log(fruits.length);   // 10 : aucune méthode n'a modifié le tableau

Le point clé : la question 4 est celle qui compte. find renvoie Fruit | undefined, et la seconde recherche montre que ce undefined n’est pas théorique. Sans le test, TypeScript refuse tresCher.nom ; avec un as Fruit pour le faire taire, le programme planterait à l’exécution. Le test explicite est la seule bonne réponse.

La question 6 montre ce que join fait à votre place. La version naïve (acc, f) => acc + ", " + f.nom avec "" comme valeur initiale produit ", pomme, kiwi, …" : au premier tour, l’accumulateur est vide et la virgule est ajoutée devant le premier fruit. D’où le test acc === "", qui traite à part le tout premier élément. En pratique, écrivez map puis join, qui est fait pour ça.

À remarquer : cerise est bien le premier fruit à plus de 5 €, même si fraise vient après : find s’arrête au premier succès. Et comparez les questions 1 et 5 : les deux parcourent tout le tableau, mais map produit un tableau qu’il faut récupérer, là où forEach ne produit rien et n’existe que pour son effet.

Exercice 2.8 : Choisir la bonne méthode

essentiel

Pour chacun des besoins ci-dessous, écrivez une seule instruction et justifiez le choix de la méthode en une phrase. Le tableau de départ :

interface Copie {
  eleve: string;
  note: number;
}

const copies: Copie[] = [
  { eleve: "Ada", note: 12 },
  { eleve: "Brice", note: 8 },
  { eleve: "Chloé", note: 17 },
  { eleve: "Dan", note: 4 },
  { eleve: "Emma", note: 15 },
  { eleve: "Farid", note: 10 },
];
  1. La moyenne des notes.
  2. Les copies notées strictement au-dessus de 10.
  3. La meilleure copie (l’objet entier, pas seulement la note).
  4. Le nombre d’élèves ayant la moyenne (10 ou plus).
  5. Les mêmes copies, avec la note ramenée sur 5 plutôt que sur 20.
  6. La copie de Dan, puis celle de Zoé, qui n’a pas composé : affichez sa note, ou absente si elle n’existe pas.

Réussite : aucune boucle for, la question 3 est résolue avec reduce, et copies n’est jamais modifié.

copies.ts
interface Copie {
  eleve: string;
  note: number;
}

const copies: Copie[] = [
  { eleve: "Ada", note: 12 },
  { eleve: "Brice", note: 8 },
  { eleve: "Chloé", note: 17 },
  { eleve: "Dan", note: 4 },
  { eleve: "Emma", note: 15 },
  { eleve: "Farid", note: 10 },
];

// 1. reduce pour la somme, puis division : une liste devient une valeur
const moyenne: number = copies.reduce((acc, c) => acc + c.note, 0) / copies.length;
console.log(moyenne);   // 11

// 2. filter : je veux les copies elles-mêmes, en moins nombreuses
const auDessus: Copie[] = copies.filter((c) => c.note > 10);
console.log(auDessus.map((c) => c.eleve).join(", "));   // Ada, Chloé, Emma

// 3. reduce : l'accumulateur est « la meilleure copie vue jusqu'ici »
const meilleure: Copie = copies.reduce((best, c) => (c.note > best.note ? c : best), copies[0]);
console.log(meilleure.eleve, meilleure.note);   // Chloé 17

// 4. filter puis .length : je sélectionne, puis je compte
const nbMoyenne: number = copies.filter((c) => c.note >= 10).length;
console.log(nbMoyenne);   // 4

// 5. map : autant de copies en sortie, chacune transformée (sans toucher à l'original)
const sur5: Copie[] = copies.map((c) => ({ ...c, note: c.note / 4 }));
console.log(sur5[2].note, copies[2].note);   // 4.25 17

// 6. find : une seule copie, ou undefined
const dan: Copie | undefined = copies.find((c) => c.eleve === "Dan");
console.log(dan !== undefined ? dan.note : "absente");   // 4

const zoe: Copie | undefined = copies.find((c) => c.eleve === "Zoé");
console.log(zoe !== undefined ? zoe.note : "absente");   // absente

Les justifications, qui sont le véritable objet de l’exercice :

  1. reduce : je pars de six copies et je veux un seul nombre. La division par copies.length vient après la réduction.
  2. filter : je veux les copies elles-mêmes, pas une transformation.
  3. reduce : encore une liste ramenée à une valeur, sauf que cette valeur est un objet : l’accumulateur n’est pas une somme mais « le meilleur vu jusqu’ici ».
  4. filter puis .length : je sélectionne, puis je compte. Notez le >= 10 : « avoir la moyenne » inclut 10, contrairement à la question 2.
  5. map : six copies en entrée, six en sortie, chacune transformée. La décomposition { ...c, note: … } fabrique une nouvelle copie au lieu de modifier l’originale.
  6. find : je veux une copie, la première qui convient, et je dois prévoir qu’il n’y en ait aucune.

Le point clé : à la question 3, la valeur initiale est copies[0], et non 0. Il s’agit de comparer des copies, et un nombre ne serait pas du bon type. La valeur initiale d’un reduce a toujours le type du résultat : 0 pour une somme, "" pour une concaténation, un élément du tableau pour un maximum.

L’erreur classique, à la question 5 : écrire copies.map((c) => { c.note = c.note / 4; return c; }). Tout compile, sur5 semble correct… et copies a été modifié en même temps, puisque c désigne l’objet d’origine. map ne protège pas les objets du tableau : c’est votre fonction qui doit en fabriquer de nouveaux.

Exercice 2.9 : Prédire avant d’exécuter

entraînement

Cet exercice entraîne la lecture de code, celle que demandent les questions « que vaut… » des évaluations. Pour chacune des cinq expressions, écrivez sur papier son résultat et son type, sans rien exécuter. Vérifiez ensuite avec console.log.

interface Article {
  nom: string;
  prix: number;
  quantite: number;
}

const panier: Article[] = [
  { nom: "stylo", prix: 2, quantite: 3 },
  { nom: "cahier", prix: 4, quantite: 2 },
  { nom: "sac", prix: 30, quantite: 1 },
  { nom: "gomme", prix: 1, quantite: 0 },
];
  1. panier.filter((a) => a.quantite > 0).map((a) => a.nom)
  2. panier.map((a) => a.prix * a.quantite).reduce((acc, x) => acc + x, 0)
  3. panier.find((a) => a.prix > 100)
  4. panier.reduce((acc, a) => acc + a.nom[0], "")
  5. panier.map((a) => { a.prix * 2; })

Réussite : au moins quatre prédictions exactes sur cinq, types compris. Pour chaque erreur, vous savez dire quel maillon vous avez mal lu.

# Résultat Type
1 ["stylo", "cahier", "sac"] string[]
2 44 number
3 undefined Article | undefined
4 "scsg" string
5 [undefined, undefined, undefined, undefined] void[]
prediction.ts
interface Article {
  nom: string;
  prix: number;
  quantite: number;
}

const panier: Article[] = [
  { nom: "stylo", prix: 2, quantite: 3 },
  { nom: "cahier", prix: 4, quantite: 2 },
  { nom: "sac", prix: 30, quantite: 1 },
  { nom: "gomme", prix: 1, quantite: 0 },
];

console.log(panier.filter((a) => a.quantite > 0).map((a) => a.nom));
console.log(panier.map((a) => a.prix * a.quantite).reduce((acc, x) => acc + x, 0));
console.log(panier.find((a) => a.prix > 100));
console.log(panier.reduce((acc, a) => acc + a.nom[0], ""));
console.log(panier.map((a) => { a.prix * 2; }));

Le détail, maillon par maillon :

  1. filter élimine la gomme (quantité 0), puis map remplace chaque article par son nom : on passe de Article[] à string[].
  2. map produit [6, 8, 30, 0], puis reduce les additionne à partir de 0. La gomme compte pour 0 mais reste dans le tableau : map garde toujours la longueur.
  3. Aucun article ne coûte plus de 100 € : find renvoie undefined. C’est un résultat normal, que le type annonce.
  4. L’accumulateur est une chaîne (valeur initiale "") à laquelle on ajoute la première lettre de chaque nom : s, c, s, g.
  5. Le piège du chapitre : des accolades sans return. La fonction ne renvoie rien, donc map produit un tableau de quatre undefined, de type void[].

Le point clé : pour lire une chaîne, on suit le type d’un maillon à l’autre. Dès que vous savez que la sortie de l’étape 1 est un string[], vous savez aussi qu’on ne peut plus y lire a.prix. Les erreurs de prédiction viennent presque toujours d’un maillon dont on a mal lu le type de sortie.

Exercice 2.10 : Statistiques sur un catalogue

entraînement transfert

Une librairie vous confie son catalogue :

type Categorie = "roman" | "bd" | "essai";

interface Livre {
  titre: string;
  categorie: Categorie;
  prix: number;
}

const catalogue: Livre[] = [
  { titre: "Dune", categorie: "roman", prix: 12.5 },
  { titre: "Maus", categorie: "bd", prix: 32 },
  { titre: "Sapiens", categorie: "essai", prix: 24 },
  { titre: "Persepolis", categorie: "bd", prix: 28 },
  { titre: "L'Étranger", categorie: "roman", prix: 8 },
  { titre: "Blast", categorie: "bd", prix: 25 },
];

Écrivez :

  1. le prix moyen du catalogue, affiché avec deux décimales ;
  2. le livre le plus cher ;
  3. le nombre de livres par catégorie, sous la forme d’un seul objet { roman: 2, bd: 3, essai: 1 }, avec un seul reduce ;
  4. une fonction prixMoyen(livres: Livre[], categorie: Categorie): number | undefined, qui renvoie undefined quand la catégorie n’a aucun livre ; testez-la sur "bd", puis sur un catalogue sans essai.

Réussite : aucune boucle for, et la fonction de la question 4 ne renvoie jamais NaN.

L’accumulateur est un objet qui part de { roman: 0, bd: 0, essai: 0 }. À chaque livre, on augmente le compteur de sa catégorie : acc[livre.categorie].

statistiques.ts
type Categorie = "roman" | "bd" | "essai";

interface Livre {
  titre: string;
  categorie: Categorie;
  prix: number;
}

interface Comptes {
  roman: number;
  bd: number;
  essai: number;
}

const catalogue: Livre[] = [
  { titre: "Dune", categorie: "roman", prix: 12.5 },
  { titre: "Maus", categorie: "bd", prix: 32 },
  { titre: "Sapiens", categorie: "essai", prix: 24 },
  { titre: "Persepolis", categorie: "bd", prix: 28 },
  { titre: "L'Étranger", categorie: "roman", prix: 8 },
  { titre: "Blast", categorie: "bd", prix: 25 },
];

// 1. Moyenne : somme par reduce, puis division
const moyenne: number = catalogue.reduce((acc, l) => acc + l.prix, 0) / catalogue.length;
console.log(moyenne.toFixed(2));   // 21.58

// 2. Maximum : l'accumulateur est un Livre
const plusCher: Livre = catalogue.reduce((max, l) => (l.prix > max.prix ? l : max), catalogue[0]);
console.log(plusCher.titre);   // Maus

// 3. Regroupement : l'accumulateur est un objet de compteurs
const parCategorie: Comptes = catalogue.reduce(
  (acc, l) => {
    acc[l.categorie] = acc[l.categorie] + 1;
    return acc;
  },
  { roman: 0, bd: 0, essai: 0 }
);
console.log(parCategorie);   // { roman: 2, bd: 3, essai: 1 }

// 4. Moyenne d'une catégorie, ou undefined si elle est vide
function prixMoyen(livres: Livre[], categorie: Categorie): number | undefined {
  const choisis: Livre[] = livres.filter((l) => l.categorie === categorie);
  if (choisis.length === 0) {
    return undefined;
  }
  return choisis.reduce((acc, l) => acc + l.prix, 0) / choisis.length;
}

console.log(prixMoyen(catalogue, "bd"));   // 28.333333333333332

const sansEssai: Livre[] = catalogue.filter((l) => l.categorie !== "essai");
const moyenneEssai = prixMoyen(sansEssai, "essai");
console.log(moyenneEssai === undefined ? "aucun essai" : moyenneEssai);   // aucun essai

Le point clé : les trois premières questions sont trois reduce, avec trois types d’accumulateur différents : un nombre (la somme), un élément du tableau (le maximum), un objet construit au fil du parcours (les compteurs). C’est la valeur initiale qui fixe ce type, et c’est elle qu’il faut choisir en premier.

À la question 3, le corps de la fonction a des accolades : le return acc; est indispensable. Sans lui, l’accumulateur vaut undefined dès le deuxième livre, et TypeScript le signale.

La question 4 est le vrai transfert : diviser par choisis.length quand il vaut 0 donne NaN, sans aucune erreur. Le type de retour number | undefined oblige l’appelant à traiter le cas « catégorie vide », exactement comme find. La fonction reçoit le tableau en paramètre plutôt que de lire catalogue directement : c’est ce qui permet de la tester sur un autre catalogue.

Exercice 2.11 : Enchaînement et affichage dans la page

approfondissement

Dans un projet Astro, à partir du tableau fruits de l’exercice 2.7 :

  1. gardez les fruits en stock ;
  2. transformez-les en pomme : 2.5 €/kg, 40 kg en stock ;
  3. affichez chacune de ces phrases dans un <li> d’une liste créée en TypeScript.

Écrivez le traitement en un seul enchaînement filter(...).map(...), puis un forEach pour l’insertion dans le DOM.

Réussite : la liste apparaît dans la page, et le forEach ne contient que du code d’affichage, aucun calcul.

src/pages/fruits.astro
---
---
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <title>Fruits</title>
</head>
<body>
    <h1>Fruits en stock</h1>
    <div id="conteneur-fruits"></div>
    <script src="../scripts/fruits.ts"></script>
</body>
</html>
src/scripts/fruits.ts
interface Fruit {
  nom: string;
  prix: number;
  stock: number;
}

const fruits: Fruit[] = [
  { nom: "pomme", prix: 2.5, stock: 40 },
  { nom: "kiwi", prix: 4, stock: 0 },
  { nom: "banane", prix: 1.8, stock: 25 },
  { nom: "cerise", prix: 9, stock: 5 },
  { nom: "orange", prix: 2.2, stock: 0 },
  { nom: "poire", prix: 3, stock: 12 },
  { nom: "fraise", prix: 7.5, stock: 8 },
  { nom: "prune", prix: 3.5, stock: 0 },
  { nom: "ananas", prix: 3.9, stock: 6 },
  { nom: "pêche", prix: 4.2, stock: 15 },
];

// 1. et 2. Tout le calcul, en un seul enchaînement
const phrases: string[] = fruits
  .filter((f) => f.stock > 0)
  .map((f) => `${f.nom} : ${f.prix} €/kg, ${f.stock} kg en stock`);

// 3. L'affichage, et rien d'autre
const conteneur = document.getElementById("conteneur-fruits");

if (conteneur !== null) {
  const liste: HTMLUListElement = document.createElement("ul");

  phrases.forEach((phrase) => {
    const element: HTMLLIElement = document.createElement("li");
    element.textContent = phrase;
    liste.appendChild(element);
  });

  conteneur.appendChild(liste);   // une seule insertion dans la page
}

La page affiche sept lignes, de pomme : 2.5 €/kg, 40 kg en stock à pêche : 4.2 €/kg, 15 kg en stock ; le kiwi, l’orange et la prune sont épuisés.

Le point clé : la contrainte « le forEach ne contient aucun calcul » n’est pas une coquetterie de style. Le code est coupé en deux moitiés nettes : d’abord les données (filter puis map produisent un string[] de phrases toutes prêtes), ensuite l’affichage (forEach se contente de fabriquer un <li> par phrase). Vous pouvez afficher console.log(phrases) pour vérifier la première moitié sans ouvrir un navigateur, et remplacer la seconde par un tableau HTML sans toucher à la première. Mélangez les deux (un if de filtrage à l’intérieur du forEach) et vous perdez cette indépendance : la logique métier devient indissociable du DOM.

C’est aussi la raison pour laquelle forEach est le bon choix ici, alors que map est préférable partout ailleurs : on ne veut produire aucune valeur, seulement agir sur la page. Un map à cet endroit fabriquerait un tableau de <li> que personne ne récupère.

Sur la performance : le <ul> est créé avant la boucle et inséré dans la page après. Les sept <li> sont donc ajoutés à un élément encore détaché du document, ce qui ne coûte rien ; le navigateur ne recalcule la mise en page qu’une fois, au conteneur.appendChild(liste) final. Insérer chaque <li> directement dans la page provoquerait sept recalculs. Sur sept éléments la différence est invisible, sur mille elle ne l’est plus du tout.

QCM

1. Que fait map ici ?

const nombres = [1, 2, 3, 4, 5];
nombres.map((nombre) => nombre ** 2);

A. Il transforme chaque élément du tableau sur place. B. Il crée un nouveau tableau avec les résultats de la fonction appliquée à chaque élément du tableau d’origine. C. Il vérifie si chaque élément satisfait une condition. D. Il réduit le tableau à une seule valeur.

B. Attention : ici le nouveau tableau n’est affecté à rien, il est donc immédiatement perdu, et nombres vaut toujours [1, 2, 3, 4, 5].

2. Que vaut resultat ?

const resultat = [1, 2, 3].forEach((x) => x * 10);

A. [10, 20, 30] B. 60 C. undefined D. [1, 2, 3]

C. forEach renvoie toujours void. Il fallait map pour obtenir [10, 20, 30].

3. Que vaut [3, 1, 4, 1, 5].reduce((acc, x) => acc + x, 10) ?

A. 14 B. 24 C. 10 D. Une erreur.

B. L’accumulateur part de 10, puis 10+3+1+4+1+5 = 24. La valeur initiale participe au calcul.

4. Quelle méthode choisir pour obtenir le premier utilisateur majeur d’une liste ?

A. filter, puis prendre l’élément d’indice 0. B. find. C. map avec un test à l’intérieur. D. reduce.

B. filter donnerait le bon résultat mais parcourrait tout le tableau et construirait un tableau inutile. find s’arrête au premier succès : pensez simplement à traiter le cas undefined.

5. Que vaut texte ?

const eleves = [{ nom: "Ada" }, { nom: "Bob" }];
const texte = `Élèves : ${eleves}`;

A. "Élèves : Ada, Bob" B. "Élèves : Ada,Bob" C. "Élèves : [object Object],[object Object]" D. Une erreur de compilation.

C. Un objet converti en texte donne [object Object]. Pour obtenir A, il faut eleves.map((e) => e.nom).join(", ").

À retenir

  • forEach → rien · map → un tableau de même taille · filter → un tableau plus court
  • find → un élément ou undefined : traitez toujours le cas « pas trouvé »
  • reduce prend un accumulateur et une valeur initiale, qui fixe son type ; renvoyez toujours l’accumulateur
  • Aucune de ces méthodes ne modifie le tableau d’origine : récupérez le résultat
  • Pour lire une chaîne, suivez le type de sortie de chaque maillon
  • Pour afficher un tableau d’objets : map vers du texte, puis join