Quand on découvre Set en JavaScript, on imagine souvent un comportement simple : si deux valeurs se ressemblent, le set n’en garde qu’une. C’est vrai pour les primitives les plus évidentes, mais ça casse très vite avec les objets. Deux objets qui ont exactement les mêmes propriétés, dans le même ordre, avec les mêmes valeurs, sont quand même considérés comme différents. C’est là que commence la vraie question : comment obtenir une unicité logique quand Set ne compare pas le contenu profond des objets ?
Le sujet peut paraître anecdotique, pourtant il revient partout : déduplication de résultats d’API, fusion de filtres, mise en cache, construction de listes uniques côté front. Si vous êtes tombé sur ce comportement en manipulant des collections, vous pouvez aussi jeter un oeil à cet article lié pour rester dans la même famille de problèmes JavaScript.
Contenu
Pourquoi deux objets identiques restent différents dans un Set ?
Set n’utilise pas un deep compare. Il utilise une logique d’identité de référence pour les objets. En clair, JavaScript ne se demande pas : « est-ce que ces deux objets ont le même contenu ? » Il se demande : « est-ce que c’est exactement le même objet en mémoire ? »
const a = { id: 1, name: 'Ada' };
const b = { id: 1, name: 'Ada' };
const set = new Set([a, b]);
console.log(set.size); // 2
Le résultat surprend souvent, mais il est cohérent avec le fonctionnement du langage. a et b se ressemblent, oui, mais ce sont deux références distinctes. À l’inverse, si vous ajoutez deux fois la même variable, Set n’en gardera qu’une.
const user = { id: 1, name: 'Ada' };
const set = new Set([user, user]);
console.log(set.size); // 1
Autrement dit, Set garantit l’unicité des références, pas l’unicité métier. C’est une nuance importante. Si votre règle d’unicité dépend d’un id, d’une combinaison de champs, ou d’une structure complète, Set seul ne suffit pas.
Pourquoi JavaScript ne fait pas ce deep compare automatiquement ?
const a = { id: 1 }
const b = { id: 1 }
const set = new Set([a, b])
console.log(set.size) // 2
Parce que ce serait coûteux, ambigu, et parfois même contre-productif. Comparer profondément deux objets n’est pas une opération gratuite. Il faut parcourir les propriétés, gérer les objets imbriqués, les tableaux, les dates, parfois les cycles. Et surtout, il faut décider ce qui compte comme « égal ». Est-ce que l’ordre des clés compte ? Est-ce que undefined doit être traité comme une absence de propriété ? Est-ce qu’une date et sa version sérialisée doivent être vues comme équivalentes ?
Le langage préfère donc une règle simple et prévisible : pour les objets, l’égalité dans un Set repose sur la référence. C’est brut, mais c’est stable.
Quelles options pour simuler un deep compare dans un Set ?
En pratique, il y a trois approches courantes.
La première consiste à dédupliquer sur une clé métier. Si chaque objet possède un identifiant fiable, c’est souvent la meilleure solution.
const users = [
{ id: 1, name: 'Ada' },
{ id: 1, name: 'Ada' },
{ id: 2, name: 'Linus' }
];
const unique = [...new Map(users.map(user => [user.id, user])).values()];
Ici, on ne compare pas tout l’objet. On décide que id définit l’unicité. C’est souvent plus clair qu’un deep compare complet.
La deuxième approche consiste à sérialiser les objets avant de les passer dans un Set.
const unique = [...new Set(users.map(user => JSON.stringify(user)))].map(str => JSON.parse(str));
Ça marche pour des cas simples, mais il faut rester prudent. JSON.stringify a des limites : les fonctions disparaissent, undefined est ignoré dans certains cas, les dates deviennent des chaînes, et l’ordre des clés peut vous piéger si les objets ne sont pas normalisés. C’est pratique pour un script rapide, moins pour une logique sensible.
La troisième approche, plus propre quand les cas deviennent sérieux, consiste à écrire une fonction de comparaison ou à utiliser une librairie spécialisée. Vous parcourez alors la liste manuellement et vous n’ajoutez un objet que s’il n’existe pas déjà selon votre propre définition de l’égalité.
const deepEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b);
const unique = [];
for (const item of users) {
if (!unique.some(existing => deepEqual(existing, item))) {
unique.push(item);
}
}
Ce n’est pas un vrai Set au sens strict, mais c’est souvent la bonne réponse au besoin réel.
Le piège du faux deep compare
const unique = new Map()
for (const item of items) {
unique.set(JSON.stringify(item), item)
}
const result = [...unique.values()]
Beaucoup de développeurs disent « je vais faire un deep compare » alors qu’ils font juste une comparaison JSON. Ce n’est pas toujours équivalent. Pour des objets plats issus d’une API bien propre, ça peut suffire. Pour des données plus riches, non. Si votre logique d’unicité a de l’importance, mieux vaut définir explicitement la règle : unicité par id, par couple type + value, ou par structure normalisée.
C’est aussi pour ça que le vrai sujet n’est pas seulement Set. Le vrai sujet, c’est : qu’est-ce qu’un doublon dans votre domaine ? La réponse technique découle de cette réponse métier. Sur un thème proche, cet autre article peut prolonger la réflexion si vous creusez les comportements parfois contre-intuitifs du langage.
Quand garder Set, et quand changer d’approche ?
Gardez Set si vous manipulez surtout des primitives, ou si vous voulez éliminer des doublons de référence. C’est rapide, lisible, natif. En revanche, dès que vous avez des objets recréés à la volée, reçus de plusieurs sources, ou reconstruits à chaque rendu, il faut sortir du réflexe new Set(array).
Dans ces cas-là, un Map indexé par clé métier est souvent plus solide. Une fonction de déduplication maison peut aussi être plus honnête qu’un pseudo deep compare caché dans un coin. Le point important, c’est d’assumer la règle plutôt que d’espérer que Set lise dans vos intentions.
Si on résume sans faire de grande morale : Set ne sait pas reconnaître deux objets « égaux » au sens humain du terme. Il reconnaît seulement deux références identiques. Dès qu’on a compris ça, on arrête de lutter contre le langage et on choisit une stratégie de déduplication adaptée au vrai besoin.