Front-end angularforall.com

- Array methods JS : map, filter, reduce, find

Javascript Array Map Filter Reduce
Array methods JS : map, filter, reduce, find

Maîtrisez les méthodes de tableau JavaScript essentielles : map(), filter(), reduce(), find() et forEach() pour une programmation fonctionnelle et efficace.

map() — transformer sans muter le tableau source

Array.prototype.map() applique une fonction à chaque élément et retourne un nouveau tableau de même longueur. Le tableau original n'est jamais modifié. La fonction de callback reçoit trois arguments : la valeur, l'index et le tableau complet.

// Signature complète du callback : (value, index, array) => newValue
const prices = [10.5, 20.0, 15.75, 8.99];

// Transformation simple
const withTax = prices.map(price => price * 1.2);
// [12.6, 24.0, 18.9, 10.788]

// Utilisation de l'index pour enrichir les données
const indexed = prices.map((price, i) => ({
    id: i + 1,
    price,
    priceFormatted: `${price.toFixed(2)} €`,
}));
// [{ id: 1, price: 10.5, priceFormatted: '10.50 €' }, ...]

map() avec des objets — cas d'usage API

// Réponse brute de l'API (snake_case, dates en string)
const apiUsers = [
    { user_id: 1, first_name: 'Alice', last_name: 'Martin', created_at: '2024-01-15' },
    { user_id: 2, first_name: 'Bob', last_name: 'Dupont', created_at: '2024-03-22' },
];

// Normalisation vers le modèle interne (camelCase, Date objects)
const users = apiUsers.map(u => ({
    id: u.user_id,
    firstName: u.first_name,
    lastName: u.last_name,
    fullName: `${u.first_name} ${u.last_name}`,
    createdAt: new Date(u.created_at),
}));
// Chaque appel API passe par ce mapper — jamais de données brutes dans les composants

Piège courant : map() vs forEach()

// MAUVAISE pratique : utiliser forEach pour collecter dans un tableau externe
const results = [];
items.forEach(item => results.push(transform(item))); // mutable, verbeux

// BONNE pratique : map() retourne directement le tableau transformé
const results = items.map(item => transform(item));

// AUTRE piège : map() qui ne retourne rien → tableau de undefined
const broken = [1, 2, 3].map(n => { n * 2; }); // [undefined, undefined, undefined]
// Il manque le return (ou la suppression des accolades pour l'arrow function implicite)

filter() — prédicats précis et type guards

filter() retourne un nouveau tableau contenant uniquement les éléments pour lesquels le prédicat retourne true. Le tableau peut être plus court, mais les éléments conservés ne sont jamais transformés.

const products = [
    { id: 1, name: 'Laptop', price: 999, inStock: true, category: 'electronics' },
    { id: 2, name: 'Desk', price: 350, inStock: false, category: 'furniture' },
    { id: 3, name: 'Phone', price: 699, inStock: true, category: 'electronics' },
    { id: 4, name: 'Chair', price: 180, inStock: true, category: 'furniture' },
];

// Filtre simple
const inStock = products.filter(p => p.inStock);
// 3 produits

// Filtre composé — prédicat avec logique AND
const affordableElectronics = products.filter(
    p => p.inStock && p.category === 'electronics' && p.price < 800
);
// [{ id: 3, name: 'Phone', price: 699, ... }]

Type guards avec filter() en TypeScript

// Problème sans type guard : le type du tableau reste trop large
const mixed: (string | null | undefined)[] = ['Alice', null, 'Bob', undefined, 'Charlie'];
const filtered = mixed.filter(x => x !== null && x !== undefined);
// TypeScript infère (string | null | undefined)[] — pas encore string[]

// Solution : type guard explicite
function isNonNullable<T>(value: T | null | undefined): value is T {
    return value !== null && value !== undefined;
}

const strings: string[] = mixed.filter(isNonNullable);
// TypeScript infère maintenant string[] correctement ✓
// Filtrer par type dans une union — pattern courant avec les events ou les résultats
type SuccessResult = { status: 'success'; data: User };
type ErrorResult  = { status: 'error'; message: string };
type Result = SuccessResult | ErrorResult;

const results: Result[] = [ /* ... */ ];

// Sans type guard : items est Result[] (union)
const successes = results.filter(r => r.status === 'success');

// Avec type guard : items est SuccessResult[] (type rétréci)
function isSuccess(r: Result): r is SuccessResult {
    return r.status === 'success';
}
const typedSuccesses: SuccessResult[] = results.filter(isSuccess);
typedSuccesses[0].data; // TypeScript OK — .data est accessible

reduce() — agrégations complexes

reduce() parcourt le tableau et accumule une valeur unique. C'est la méthode la plus puissante mais aussi la plus mal utilisée. Le deuxième argument (valeur initiale de l'accumulateur) est crucial pour la robustesse.

Calculs financiers précis

const orderItems = [
    { name: 'Laptop', qty: 1, unitPrice: 999.99 },
    { name: 'Mouse', qty: 2, unitPrice: 29.99 },
    { name: 'Keyboard', qty: 1, unitPrice: 79.99 },
];

// Sous-total, TVA, total
const subtotal = orderItems.reduce((sum, item) => sum + item.qty * item.unitPrice, 0);
// 1139.96

const TAX_RATE = 0.20;
const tax     = subtotal * TAX_RATE;   // 227.99
const total   = subtotal + tax;         // 1367.95

Transformation de structure — tableau vers objet indexé

interface User { id: number; name: string; email: string; department: string; }

const users: User[] = [
    { id: 1, name: 'Alice', email: 'alice@ex.com', department: 'engineering' },
    { id: 2, name: 'Bob',   email: 'bob@ex.com',   department: 'marketing' },
    { id: 3, name: 'Carol', email: 'carol@ex.com',  department: 'engineering' },
];

// Indexer par ID pour les lookups O(1)
const usersById = users.reduce<Record<number, User>>((acc, user) => {
    acc[user.id] = user;
    return acc;
}, {});
// { 1: User, 2: User, 3: User }

// Grouper par department
const byDept = users.reduce<Record<string, User[]>>((acc, user) => {
    (acc[user.department] ??= []).push(user);
    return acc;
}, {});
// { engineering: [Alice, Carol], marketing: [Bob] }

reduce() vs flatMap() pour les transformations

// Cas où reduce() est moins lisible que flatMap()
const categories = [
    { name: 'Electronics', tags: ['tech', 'gadget'] },
    { name: 'Sports', tags: ['outdoor', 'fitness', 'health'] },
];

// Avec reduce : accumulateur + concat → verbeux
const tagsByReduce = categories.reduce((acc, c) => acc.concat(c.tags), [] as string[]);

// Avec flatMap : plus lisible pour ce cas précis
const tagsByFlatMap = categories.flatMap(c => c.tags);
// ['tech', 'gadget', 'outdoor', 'fitness', 'health']

// Règle : utilise reduce quand le type de retour est DIFFÉRENT du type des éléments
// Utilise flatMap quand tu transformes ET applatit

find(), findIndex(), findLast()

find() retourne le premier élément qui satisfait le prédicat, ou undefined. findIndex() retourne l'index (ou -1). Contrairement à filter(), ils s'arrêtent dès la première correspondance — plus efficace pour les grandes collections.

const orders = [
    { id: 'ORD-001', status: 'delivered', total: 150 },
    { id: 'ORD-002', status: 'processing', total: 75 },
    { id: 'ORD-003', status: 'processing', total: 200 },
    { id: 'ORD-004', status: 'cancelled', total: 50 },
];

// find() — s'arrête au premier match
const processing = orders.find(o => o.status === 'processing');
// { id: 'ORD-002', status: 'processing', total: 75 } — ORD-003 jamais parcouru

// findIndex() — utile pour les mises à jour immuables
const idx = orders.findIndex(o => o.id === 'ORD-002');
// 1 — permet la mise à jour sans mutation : [...orders.slice(0, idx), updated, ...orders.slice(idx+1)]

// Vérification obligatoire : find() peut retourner undefined
const order = orders.find(o => o.id === 'ORD-999');
if (order) {
    console.log(order.total); // TypeScript OK — order est garanti non-undefined ici
}
// findLast() et findLastIndex() — ES2023 (Chrome 110+, Node 18+)
const lastProcessing = orders.findLast(o => o.status === 'processing');
// { id: 'ORD-003', ... } — parcourt en sens inverse, s'arrête au premier match

// Utile pour les logs, historiques, audits où on cherche l'entrée la plus récente
const transactions = [
    { id: 1, type: 'credit', amount: 100, date: '2024-01-01' },
    { id: 2, type: 'debit', amount: 50, date: '2024-01-05' },
    { id: 3, type: 'credit', amount: 200, date: '2024-01-10' },
];
const lastCredit = transactions.findLast(t => t.type === 'credit');
// { id: 3, type: 'credit', amount: 200, date: '2024-01-10' }

some(), every(), includes()

Ces trois méthodes retournent un booléen. Elles s'arrêtent dès qu'elles ont leur réponse (court-circuit), ce qui les rend efficaces sur les grandes collections.

const roles = ['viewer', 'editor', 'admin'];

// some() : au moins UN élément satisfait le prédicat — court-circuit sur le premier true
const hasPrivilegedRole = roles.some(r => r === 'admin' || r === 'superadmin');
// true — s'arrête à 'admin', ne parcourt pas jusqu'à la fin

// every() : TOUS les éléments satisfont — court-circuit sur le premier false
const allValid = roles.every(r => r.length > 0 && /^[a-z]+$/.test(r));
// true

// includes() : présence d'une valeur exacte (=== strict)
const isAdmin = roles.includes('admin'); // true
const isSuperAdmin = roles.includes('superadmin'); // false
// Différence includes() vs some() pour les cas courants
const permissions = ['read', 'write', 'delete'];

// includes() : valeur primitive — plus rapide et lisible
const canDelete = permissions.includes('delete'); // ✓ préféré pour les primitives

// some() : logique complexe ou objets
const userRoles = [{ id: 1, name: 'admin' }, { id: 2, name: 'editor' }];
const isAdmin = userRoles.some(r => r.name === 'admin'); // ✓ nécessaire pour les objets
// userRoles.includes({ name: 'admin' }) serait false (comparaison de référence)

flat() et flatMap() — tableaux imbriqués

flat(depth) applatit les tableaux imbriqués jusqu'à la profondeur spécifiée. flatMap(fn) combine un map() puis flat(1) en une seule passe — plus efficace.

// flat() avec profondeur variable
const nested = [[1, 2], [3, [4, 5]], [6]];

nested.flat();   // [1, 2, 3, [4, 5], 6] — profondeur 1 par défaut
nested.flat(2);  // [1, 2, 3, 4, 5, 6]  — profondeur 2
nested.flat(Infinity); // applatit complètement quel que soit le niveau
// flatMap() — transformer ET applatir en une seule itération
const sentences = ['Hello world', 'TypeScript is great', 'JS rocks'];
const words = sentences.flatMap(s => s.split(' '));
// ['Hello', 'world', 'TypeScript', 'is', 'great', 'JS', 'rocks']

// Supprimer des éléments pendant la transformation — astuce avec tableau vide
const numbers = [1, 2, 3, 4, 5, 6];
const evenDoubled = numbers.flatMap(n => n % 2 === 0 ? [n * 2] : []);
// [4, 8, 12] — filtre ET transforme en une seule passe
// Equivalent à : numbers.filter(n => n % 2 === 0).map(n => n * 2)
// mais plus efficace (une seule itération)

sort(), splice(), slice() — ordre et extraction

sort() — attention à la mutation et à la comparaison

// PIÈGE CRITIQUE : sort() mute le tableau original ET compare en string par défaut
const nums = [10, 2, 100, 25, 8];
nums.sort(); // [10, 100, 2, 25, 8] — tri lexicographique, pas numérique!

// Comparateur numérique correct
const sorted = [...nums].sort((a, b) => a - b); // copie d'abord, puis tri
// [2, 8, 10, 25, 100]

// Tri d'objets par propriété
const users = [
    { name: 'Charlie', age: 28 },
    { name: 'Alice', age: 32 },
    { name: 'Bob', age: 25 },
];

// Tri alphabétique stable (localeCompare respecte les accents et la locale)
const byName = [...users].sort((a, b) => a.name.localeCompare(b.name, 'fr'));
// [Alice, Bob, Charlie]

// Tri par age Descendant
const byAgeDesc = [...users].sort((a, b) => b.age - a.age);
// [Alice(32), Charlie(28), Bob(25)]

slice() vs splice()

const letters = ['a', 'b', 'c', 'd', 'e'];

// slice(start, end) — retourne une copie, NE MUTE PAS
const middle = letters.slice(1, 4); // ['b', 'c', 'd']
console.log(letters); // ['a', 'b', 'c', 'd', 'e'] — inchangé

// splice(start, deleteCount, ...items) — MUTE le tableau original
const removed = letters.splice(1, 2, 'X', 'Y');
// removed = ['b', 'c']
// letters = ['a', 'X', 'Y', 'd', 'e'] — MUTÉ

// Préfère toujours slice() dans du code fonctionnel (React, Angular state)
// N'utilise splice() que si tu sais que la mutation est intentionnelle et gérée

Chaîner les méthodes — pièges de performance

Chaque méthode chainée crée un nouveau tableau intermédiaire. Sur de grandes collections (10 000+ éléments), ce coût peut devenir significatif.

// Chaîne lisible pour les cas courants (données < 10 000 éléments)
const activeAdminNames = users
    .filter(u => u.active)                    // tableau intermédiaire 1
    .filter(u => u.role === 'admin')           // tableau intermédiaire 2
    .map(u => `${u.firstName} ${u.lastName}`) // tableau intermédiaire 3
    .sort((a, b) => a.localeCompare(b, 'fr'));

// Optimisation pour les grandes collections : reduce() combine tout en un seul passage
const activeAdminNamesOptimized = users.reduce<string[]>((acc, u) => {
    if (u.active && u.role === 'admin') {
        acc.push(`${u.firstName} ${u.lastName}`);
    }
    return acc;
}, []).sort((a, b) => a.localeCompare(b, 'fr'));
// Un seul parcours du tableau source

Typage strict avec TypeScript

TypeScript infère automatiquement les types de retour des méthodes de tableau. Comprendre ces inférences permet d'éviter les erreurs et d'écrire du code plus précis.

// TypeScript infère précisément les types de retour
const mixed: (string | number)[] = ['a', 1, 'b', 2, 'c', 3];

// filter sans type guard : type reste (string | number)[]
const noGuard = mixed.filter(x => typeof x === 'string');

// filter avec type guard : type devient string[]
function isString(x: string | number): x is string {
    return typeof x === 'string';
}
const strings: string[] = mixed.filter(isString); // string[] ✓

// map : le type de retour est inféré depuis le callback
const items = [{ id: 1, price: 10 }, { id: 2, price: 20 }];
const ids   = items.map(i => i.id);    // number[]
const prices = items.map(i => i.price); // number[]
const labels = items.map(i => `#${i.id}: ${i.price}€`); // string[]

// reduce avec type explicite — toujours annoter l'accumulateur
const total = items.reduce<number>((acc, item) => acc + item.price, 0);
// Sans <number>, TypeScript peut inférer un type trop large
Récapitulatif des méthodes par cas d'usage :
  • Transformer chaque élémentmap()
  • Filtrer selon un critèrefilter()
  • Agréger en une valeurreduce()
  • Trouver un élémentfind() / findLast()
  • Vérifier l'existencesome() / every() / includes()
  • Applatir des tableauxflat() / flatMap()

Partager