Utiliser Map en TypeScript : guide complet avec exemples

Après plus de dix ans de développement web, je peux affirmer que la structure Map est l’une des fonctionnalités les plus sous-estimées de TypeScript. Beaucoup de développeurs se contentent d’objets classiques pour stocker des paires clé-valeur, alors que Map offre des avantages considérables en termes de typage, de performance et de flexibilité. Dans ce guide, je vous propose de découvrir en profondeur comment utiliser Map en TypeScript, avec des exemples concrets tirés de mes projets quotidiens.

Dans cet article

  • La structure Map en TypeScript permet de stocker des paires clé-valeur avec un typage générique strict sur les clés et les valeurs
  • Map surpasse les objets classiques pour les collections dynamiques avec des performances O(1) en lecture et écriture
  • La méthode Array.prototype.map() et la structure Map sont deux concepts distincts à ne pas confondre
  • Les Mapped Types permettent de transformer des types existants pour créer de nouveaux types dérivés
  • Map accepte n’importe quel type comme clé, contrairement aux objets limités aux chaînes et symboles
  • Combiner Map avec les génériques TypeScript garantit une sécurité de type maximale à la compilation

Comprendre la structure Map en TypeScript

La structure Map est un objet natif de JavaScript, pleinement supporté et typé en TypeScript. Elle stocke des paires clé-valeur où chaque clé est unique. Contrairement à un objet classique, une Map accepte n’importe quel type de donnée comme clé : des nombres, des objets, des fonctions ou même d’autres Maps.

En TypeScript, Map bénéficie du système de génériques. On déclare une Map avec la syntaxe Map<K, V>, où K représente le type des clés et V le type des valeurs. Cette déclaration garantit que le compilateur vérifie chaque insertion et chaque lecture à la compilation, ce qui élimine toute une catégorie de bogues à l’exécution.

const utilisateurs: Map<number, string> = new Map();
utilisateurs.set(1, "Alice");
utilisateurs.set(2, "Bob");

const nom: string | undefined = utilisateurs.get(1);
console.log(nom); // "Alice"

Ce typage strict est un atout majeur par rapport à JavaScript pur. Si je tente d’insérer une clé de type chaîne dans une Map typée Map<number, string>, TypeScript lève immédiatement une erreur. Ce comportement s’inscrit dans la même logique que les interfaces TypeScript, qui renforcent la cohérence du code à grande échelle.

Selon la documentation MDN sur Map, cette structure a été introduite avec ECMAScript 2015 et est désormais supportée par tous les navigateurs modernes et par Node.js depuis la version 4.

Créer et manipuler une Map pas à pas

Créer une Map en TypeScript se fait de plusieurs façons. La plus courante consiste à instancier un objet Map vide puis à le remplir avec la méthode set(). On peut aussi initialiser une Map à partir d’un tableau de paires.

// Création vide
const produits: Map<string, number> = new Map();

// Création avec initialisation
const tarifs: Map<string, number> = new Map([
  ["hosting", 9.99],
  ["domaine", 12.50],
  ["ssl", 0]
]);

Les méthodes essentielles de Map sont au nombre de six. Voici comment je les utilise au quotidien dans mes projets Node.js :

set(clé, valeur) ajoute ou met à jour une entrée. Si la clé existe déjà, la valeur est remplacée sans créer de doublon.

tarifs.set("cdn", 4.99);
tarifs.set("hosting", 14.99); // mise à jour

get(clé) récupère la valeur associée à une clé. Le type de retour inclut undefined, ce qui oblige à gérer le cas où la clé n’existe pas.

const prix: number | undefined = tarifs.get("hosting");
if (prix !== undefined) {
  console.log(`Le prix est de ${prix} €`);
}

has(clé) vérifie l’existence d’une clé et retourne un booléen. C’est plus fiable que de comparer la valeur à undefined, car une Map peut légitimement contenir des valeurs undefined.

delete(clé) supprime une entrée et retourne true si l’entrée existait.

clear() vide entièrement la Map.

size retourne le nombre d’entrées. Contrairement à un objet où il faut appeler Object.keys(obj).length, la propriété size d’une Map est disponible en temps constant.

console.log(tarifs.size); // 4
tarifs.delete("cdn");
console.log(tarifs.has("cdn")); // false

Map contre objet classique : quand choisir quoi

C’est la question que me posent le plus souvent les développeurs juniors. La réponse n’est pas absolue : elle dépend du contexte. Voici un comparatif que j’ai construit à partir de mon expérience sur des projets réels.

Critère Map Objet classique
Types de clés Tous types (number, object, function) string et symbol uniquement
Ordre d’insertion Garanti Garanti depuis ES2015 (avec nuances)
Performance en ajout/suppression fréquents Optimisée Moins performante
Accès à la taille O(1) via .size O(n) via Object.keys().length
Itération Directe (for…of, forEach) Nécessite Object.entries()
Sérialisation JSON Nécessite une conversion manuelle Native avec JSON.stringify()
Prototype hérité Pas de pollution de prototype Hérite de Object.prototype
Cas d’usage idéal Caches, registres, collections dynamiques Structures de données fixes, DTOs

En résumé, je recommande Map pour les collections dynamiques où les entrées sont fréquemment ajoutées ou supprimées, et les objets pour les structures de données fixes comme les configurations ou les réponses d’API. Cette distinction rejoint les principes du DevOps, où choisir le bon outil pour le bon usage fait toute la différence.

Un point que beaucoup ignorent : Map n’a aucune pollution de prototype. Avec un objet classique, des clés comme constructor ou toString peuvent entrer en conflit avec les propriétés héritées de Object.prototype. Map élimine ce risque.

La méthode Array.map() en TypeScript

Attention à ne pas confondre la structure Map avec la méthode Array.prototype.map(). Ce sont deux concepts totalement différents qui partagent le même nom. La méthode map() des tableaux transforme chaque élément d’un tableau et retourne un nouveau tableau.

const prix: number[] = [10, 20, 30];
const prixTTC: number[] = prix.map((p: number) => p * 1.20);
console.log(prixTTC); // [12, 24, 36]

En TypeScript, le typage de map() est automatiquement inféré à partir du type de retour de la fonction callback. Si la fonction retourne un type différent du type source, TypeScript déduit correctement le nouveau type du tableau.

interface Utilisateur {
  id: number;
  nom: string;
  email: string;
}

const utilisateurs: Utilisateur[] = [
  { id: 1, nom: "Alice", email: "[email protected]" },
  { id: 2, nom: "Bob", email: "[email protected]" }
];

// Extraction des noms : TypeScript infère string[]
const noms = utilisateurs.map((u) => u.nom);

// Transformation en objet différent
const résumés = utilisateurs.map((u) => ({
  label: `${u.nom} (${u.id})`,
  contact: u.email
}));
// Type inféré : { label: string; contact: string }[]

Cette capacité d’inférence est l’un des points forts de TypeScript. Je l’utilise systématiquement dans mes projets Vue.js et React pour transformer des données avant de les afficher dans des composants. La combinaison de map() avec les interfaces TypeScript garantit que chaque transformation est correctement typée de bout en bout.

On peut aussi chaîner map() avec d’autres méthodes de tableau comme filter() ou reduce() :

const produitsActifs = produitsList
  .filter((p) => p.actif)
  .map((p) => ({ nom: p.nom, prixTTC: p.prix * 1.20 }));

Les Mapped Types : transformer des types avec Map

TypeScript propose un troisième concept lié au mot “map” : les Mapped Types. Il s’agit d’un mécanisme du système de types qui permet de créer de nouveaux types en itérant sur les propriétés d’un type existant. C’est un outil puissant pour éviter la duplication de types.

La syntaxe de base utilise l’opérateur in keyof :

type Optionnel<T> = {
  [P in keyof T]?: T[P];
};

interface Config {
  host: string;
  port: number;
  debug: boolean;
}

// Toutes les propriétés deviennent optionnelles
type ConfigPartielle = Optionnel<Config>;
// Équivalent à : { host?: string; port?: number; debug?: boolean }

TypeScript fournit plusieurs Mapped Types utilitaires intégrés, comme le précise la documentation officielle TypeScript sur les Mapped Types. Les plus utilisés sont Partial<T>, Required<T>, Readonly<T>, Pick<T, K> et Record<K, V>.

// Rendre toutes les propriétés en lecture seule
type ConfigImmuable = Readonly<Config>;

// Sélectionner certaines propriétés
type ConfigRéseau = Pick<Config, "host" | "port">;

// Créer un type avec des clés spécifiques
type StatusCodes = Record<"ok" | "erreur" | "timeout", number>;
// Équivalent à : { ok: number; erreur: number; timeout: number }

Les Mapped Types sont particulièrement utiles dans les projets à grande échelle. Dans mes applications Symfony couplées à un front TypeScript, je les utilise pour dériver automatiquement les types de formulaires à partir des types de réponse API, ce qui évite toute désynchronisation entre le back-end et le front-end.

// Type de réponse API
interface ArticleAPI {
  id: number;
  titre: string;
  contenu: string;
  datePublication: string;
  auteur: string;
}

// Type de formulaire dérivé (sans id ni datePublication)
type ArticleFormulaire = Omit<ArticleAPI, "id" | "datePublication">;
// Résultat : { titre: string; contenu: string; auteur: string }

Techniques avancées avec Map en TypeScript

Passons aux usages plus poussés. Quand on maîtrise les bases, Map devient un outil extrêmement versatile.

Map avec des objets comme clés

L’un des avantages majeurs de Map est la possibilité d’utiliser des objets comme clés. Les objets sont comparés par référence, pas par valeur :

interface Coordonnées {
  lat: number;
  lng: number;
}

const zones: Map<Coordonnées, string> = new Map();
const paris: Coordonnées = { lat: 48.8566, lng: 2.3522 };
const lyon: Coordonnées = { lat: 45.7640, lng: 4.8357 };

zones.set(paris, "Île-de-France");
zones.set(lyon, "Auvergne-Rhône-Alpes");

console.log(zones.get(paris)); // "Île-de-France"

Itérer sur une Map

Map implémente le protocole itérable, ce qui rend le parcours très naturel :

const config: Map<string, string | number> = new Map([
  ["env", "production"],
  ["port", 3000],
  ["version", "2.1.0"]
]);

// Avec for...of et déstructuration
for (const [clé, valeur] of config) {
  console.log(`${clé} = ${valeur}`);
}

// Avec forEach
config.forEach((valeur, clé) => {
  console.log(`${clé} : ${valeur}`);
});

// Extraire clés ou valeurs
const clés: IterableIterator<string> = config.keys();
const valeurs: IterableIterator<string | number> = config.values();

Convertir entre Map et objet

La sérialisation est souvent un point de friction. Voici les fonctions utilitaires que j’utilise dans tous mes projets :

// Map vers objet
function mapVersObjet<V>(map: Map<string, V>): Record<string, V> {
  return Object.fromEntries(map);
}

// Objet vers Map
function objetVersMap<V>(obj: Record<string, V>): Map<string, V> {
  return new Map(Object.entries(obj));
}

// Map vers JSON
function mapVersJSON<V>(map: Map<string, V>): string {
  return JSON.stringify(Object.fromEntries(map));
}

Map de Maps (Map imbriquées)

Pour modéliser des structures complexes, on peut imbriquer des Maps. C’est un pattern que j’utilise fréquemment pour des systèmes de cache multi-niveaux :

const cache: Map<string, Map<string, unknown>> = new Map();

function setCache(namespace: string, clé: string, valeur: unknown): void {
  if (!cache.has(namespace)) {
    cache.set(namespace, new Map());
  }
  cache.get(namespace)!.set(clé, valeur);
}

function getCache(namespace: string, clé: string): unknown | undefined {
  return cache.get(namespace)?.get(clé);
}

setCache("utilisateurs", "user_1", { nom: "Alice" });
setCache("produits", "prod_42", { prix: 29.99 });

Cas pratiques et patterns courants

Voici trois cas d’usage réels où Map brille particulièrement. Ce sont des patterns que j’implémente régulièrement dans mes projets professionnels.

Système de registre de services

Le pattern Service Registry utilise Map pour enregistrer et résoudre des dépendances. C’est la base des conteneurs d’injection de dépendances :

class ServiceRegistry {
  private services: Map<string, () => unknown> = new Map();

  register<T>(nom: string, factory: () => T): void {
    this.services.set(nom, factory);
  }

  resolve<T>(nom: string): T {
    const factory = this.services.get(nom);
    if (!factory) {
      throw new Error(`Service "${nom}" non enregistré`);
    }
    return factory() as T;
  }
}

const registry = new ServiceRegistry();
registry.register("logger", () => new ConsoleLogger());
registry.register("db", () => new DatabaseConnection());

Ce pattern s’inspire directement du conteneur de services de Symfony, un framework que je pratique depuis 2014. La version TypeScript est plus légère mais repose sur le même principe : découpler la création d’un service de son utilisation.

Compteur de fréquences avec groupBy

L’opération groupBy est l’une des recherches associées les plus courantes autour de Map. Voici une implémentation typée :

function groupBy<T, K>(tableau: T[], sélecteur: (item: T) => K): Map<K, T[]> {
  const résultat: Map<K, T[]> = new Map();
  for (const item of tableau) {
    const clé = sélecteur(item);
    const groupe = résultat.get(clé) ?? [];
    groupe.push(item);
    résultat.set(clé, groupe);
  }
  return résultat;
}

interface Commande {
  id: number;
  statut: "en_cours" | "livrée" | "annulée";
  montant: number;
}

const commandes: Commande[] = [
  { id: 1, statut: "livrée", montant: 59.99 },
  { id: 2, statut: "en_cours", montant: 120.00 },
  { id: 3, statut: "livrée", montant: 34.50 },
  { id: 4, statut: "annulée", montant: 89.00 }
];

const parStatut = groupBy(commandes, (c) => c.statut);
// Map { "livrée" => [...], "en_cours" => [...], "annulée" => [...] }

Notez que depuis ECMAScript 2024, Map.groupBy() est disponible nativement. Mais en production, je recommande encore l’implémentation manuelle tant que le support navigateur n’est pas universel, surtout si vous ciblez des environnements comme des versions anciennes de Node.js.

Cache avec expiration (TTL)

class TTLCache<K, V> {
  private cache: Map<K, { valeur: V; expiration: number }> = new Map();

  set(clé: K, valeur: V, ttlMs: number): void {
    this.cache.set(clé, {
      valeur,
      expiration: Date.now() + ttlMs
    });
  }

  get(clé: K): V | undefined {
    const entrée = this.cache.get(clé);
    if (!entrée) return undefined;
    if (Date.now() > entrée.expiration) {
      this.cache.delete(clé);
      return undefined;
    }
    return entrée.valeur;
  }
}

const cache = new TTLCache<string, string>();
cache.set("token", "abc123", 60000); // expire dans 60 secondes

Erreurs courantes et bonnes pratiques

En accompagnant des développeurs sur leurs projets TypeScript, je constate régulièrement les mêmes erreurs avec Map. Voici les pièges à éviter.

Erreur n°1 : comparer des objets par valeur. Map compare les clés objets par référence, pas par contenu. Deux objets identiques en contenu sont considérés comme deux clés distinctes :

const map = new Map<{ id: number }, string>();
map.set({ id: 1 }, "Alice");
console.log(map.get({ id: 1 })); // undefined ! Référence différente

La solution consiste à conserver une référence à l’objet ou à utiliser une clé primitive comme identifiant.

Erreur n°2 : oublier le type de retour de get(). La méthode get() retourne V | undefined. Ignorer le cas undefined provoque des erreurs à l’exécution. Utilisez toujours has() avant get(), ou l’opérateur de coalescence nulle ??.

Erreur n°3 : sérialiser directement en JSON. JSON.stringify(maMap) retourne {}. Il faut d’abord convertir avec Object.fromEntries() ou implémenter une méthode toJSON() personnalisée.

Erreur n°4 : utiliser Map quand un objet suffit. Pour des configurations statiques avec des clés connues à l’avance, un objet typé avec une interface est plus adapté. Map excelle pour les collections dynamiques dont la taille varie à l’exécution. Si vous travaillez avec des types énumérés, les enum TypeScript combinés à des objets sont souvent plus lisibles.

Bonne pratique : toujours typer explicitement. Même si TypeScript peut inférer les types, je recommande de déclarer explicitement les paramètres génériques de Map. Cela rend le code plus lisible pour les autres développeurs de l’équipe et facilite la maintenance à long terme. Cette approche s’inscrit dans une démarche de DevOps où la lisibilité du code réduit les frictions entre développeurs.

Bonne pratique : utiliser WeakMap pour les métadonnées. Si vous attachez des données à des objets sans vouloir empêcher le ramasse-miettes de les collecter, utilisez WeakMap plutôt que Map. La WeakMap documentée par MDN est idéale pour stocker des métadonnées privées associées à des éléments DOM ou des instances de classe.

Bonne pratique : préférer ReadonlyMap pour les données immuables. TypeScript fournit le type ReadonlyMap<K, V> qui expose uniquement les méthodes de lecture (get, has, size, forEach). C’est parfait pour exposer une Map en lecture seule depuis une API publique :

class ConfigurationService {
  private _config: Map<string, string> = new Map();

  get config(): ReadonlyMap<string, string> {
    return this._config;
  }
}

À retenir

  • Utilisez Map<K, V> pour les collections dynamiques de paires clé-valeur avec un typage strict
  • Vérifiez toujours l’existence d’une clé avec has() avant d’appeler get() pour éviter les erreurs à l’exécution
  • Convertissez via Object.fromEntries() avant de sérialiser une Map en JSON
  • Préférez ReadonlyMap pour exposer des données en lecture seule depuis vos services
  • Réservez Map aux collections dynamiques et utilisez des objets typés pour les structures de données fixes

Questions fréquentes


Quelle est la différence entre Map et un objet en TypeScript ?

Map accepte n’importe quel type comme clé (nombres, objets, fonctions), garantit l’ordre d’insertion, offre un accès O(1) à la taille via la propriété size, et n’a aucune pollution de prototype. Un objet classique ne supporte que des clés de type chaîne ou symbole, mais se sérialise nativement en JSON avec JSON.stringify(). Map est idéale pour les collections dynamiques, l’objet pour les structures fixes.


Comment convertir une Map en JSON en TypeScript ?

Utilisez JSON.stringify(Object.fromEntries(maMap)) pour convertir une Map avec des clés de type chaîne en JSON. Pour des clés non-chaînes, il faut d’abord transformer les clés en chaînes ou utiliser un tableau de paires : JSON.stringify(Array.from(maMap.entries())). L’opération inverse se fait avec new Map(JSON.parse(jsonString)).


Array.map() et new Map() sont-ils liés en TypeScript ?

Non, ce sont deux concepts totalement distincts. Array.prototype.map() est une méthode de tableau qui transforme chaque élément et retourne un nouveau tableau. new Map() crée une structure de données de type dictionnaire pour stocker des paires clé-valeur. Le seul point commun est le nom “map” qui fait référence au concept mathématique de correspondance.


Qu’est-ce qu’un Mapped Type en TypeScript ?

Un Mapped Type est un mécanisme du système de types de TypeScript qui permet de créer un nouveau type en itérant sur les propriétés d’un type existant. La syntaxe [P in keyof T]: T[P] parcourt chaque propriété. Les utilitaires intégrés comme Partial, Required, Readonly et Pick sont des Mapped Types. Ils évitent la duplication de types dans les grands projets.


Quand utiliser WeakMap plutôt que Map en TypeScript ?

Utilisez WeakMap quand les clés sont des objets et que vous ne voulez pas empêcher le ramasse-miettes de les collecter. C’est idéal pour attacher des métadonnées privées à des éléments DOM, stocker des données temporaires sur des instances de classe, ou implémenter des propriétés privées. WeakMap n’est pas itérable et n’a pas de propriété size, ce qui la rend inadaptée aux cas où il faut parcourir les entrées.


Comment utiliser groupBy avec Map en TypeScript ?

Depuis ECMAScript 2024, la méthode statique Map.groupBy(itérable, callback) permet de regrouper des éléments directement. En attendant un support universel, vous pouvez implémenter une fonction groupBy qui crée une Map, itère sur le tableau source, et regroupe les éléments par la clé retournée par la fonction de sélection. Le résultat est une Map<K, T[]> où chaque entrée contient le groupe d’éléments correspondants.


Damien Roux
Damien Roux

Ingénieur système et expert hébergement web. Fondateur de web-city.fr, il partage guides pratiques, comparatifs objectifs et outils gratuits pour choisir le bon hébergeur et créer son site WordPress.

Scroll to Top