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,findetreduceselon 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 :
undefineddefind,reducesans valeur initiale,mapqui 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 intactLes 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, ÉcranLa 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); // 2160Le 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); // ÉcranQuand l’utiliser : somme, moyenne, maximum, concaténation, comptage par catégorie : tout ce qui condense une liste en un seul résultat.
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); // 1260Filtrez 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 lirep.prix; après unmapversstring[], les propriétés du produit ne sont plus accessibles ; aprèsreduceoujoin, 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); // correctreduce 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 undefinedSi 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, AgendaExercices
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 },
];map: un tableau des noms en majuscules.filter: les fruits en stock.reduce: la valeur totale du stock (prix × stock, pour tous les fruits).find: le premier fruit à plus de 5 € le kilo. Puis le premier à plus de 20 € : affichezaucuns’il n’y en a pas.forEach: afficher chaque fruit précédé de son rang,1. pomme (2.5 €/kg), etc.- Une seule chaîne contenant tous les noms séparés par
", ", d’abord avecmapetjoin, puis avecreduceseul.
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 tableauLe 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 },
];- La moyenne des notes.
- Les copies notées strictement au-dessus de 10.
- La meilleure copie (l’objet entier, pas seulement la note).
- Le nombre d’élèves ayant la moyenne (10 ou plus).
- Les mêmes copies, avec la note ramenée sur 5 plutôt que sur 20.
- La copie de Dan, puis celle de Zoé, qui n’a pas composé : affichez sa note, ou
absentesi 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"); // absenteLes justifications, qui sont le véritable objet de l’exercice :
reduce: je pars de six copies et je veux un seul nombre. La division parcopies.lengthvient après la réduction.filter: je veux les copies elles-mêmes, pas une transformation.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 ».filterpuis.length: je sélectionne, puis je compte. Notez le>= 10: « avoir la moyenne » inclut 10, contrairement à la question 2.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.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 },
];panier.filter((a) => a.quantite > 0).map((a) => a.nom)panier.map((a) => a.prix * a.quantite).reduce((acc, x) => acc + x, 0)panier.find((a) => a.prix > 100)panier.reduce((acc, a) => acc + a.nom[0], "")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 :
filterélimine la gomme (quantité 0), puismapremplace chaque article par son nom : on passe deArticle[]àstring[].mapproduit[6, 8, 30, 0], puisreduceles additionne à partir de0. La gomme compte pour 0 mais reste dans le tableau :mapgarde toujours la longueur.- Aucun article ne coûte plus de 100 € :
findrenvoieundefined. C’est un résultat normal, que le type annonce. - L’accumulateur est une chaîne (valeur initiale
"") à laquelle on ajoute la première lettre de chaque nom :s,c,s,g. - Le piège du chapitre : des accolades sans
return. La fonction ne renvoie rien, doncmapproduit un tableau de quatreundefined, de typevoid[].
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 :
- le prix moyen du catalogue, affiché avec deux décimales ;
- le livre le plus cher ;
- le nombre de livres par catégorie, sous la forme d’un seul objet
{ roman: 2, bd: 3, essai: 1 }, avec un seulreduce; - une fonction
prixMoyen(livres: Livre[], categorie: Categorie): number | undefined, qui renvoieundefinedquand 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 essaiLe 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 :
- gardez les fruits en stock ;
- transformez-les en
pomme : 2.5 €/kg, 40 kg en stock; - 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 courtfind→ un élément ouundefined: traitez toujours le cas « pas trouvé »reduceprend 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 :
mapvers du texte, puisjoin