TechEarl

Remove Duplicate Values From an Array in JavaScript

Dedupe a JavaScript array. [...new Set(arr)] is the one-liner for primitives, but Set compares objects by reference, so arrays of objects need a Map keyed on a property. The gotchas with NaN and signed zero too.

Ishan Karunaratne⏱️ 6 min readUpdated
Share thisCopied
Remove duplicate values from a JavaScript array with new Set for primitives and a Map keyed on a property for arrays of objects, plus the NaN and signed-zero semantics.

To remove duplicate values from an array of primitives in JavaScript, wrap it in a Set and spread it back into an array:

javascript
const unique = [...new Set([1, 2, 2, 3, 3, 3])]; // [1, 2, 3]

That is the whole answer for numbers, strings, booleans, null, undefined, and symbols. A Set stores each value at most once, so building one from the array drops the repeats; the spread (...) turns it back into a plain array. It preserves first-seen order, which is the behavior you almost always want.

The rest of this page is the part the one-liner does not tell you: it silently fails on arrays of objects, because Set compares objects by reference, not by contents. If you have [{id: 1}, {id: 1}], Set keeps both. I have watched that "fix" ship and quietly do nothing more than once, so the object case gets its own section below.

Dedupe an array of primitives

The canonical form, and the one to reach for by default:

javascript
const ids = [4, 4, 8, 15, 16, 16, 23, 42, 42];
const unique = [...new Set(ids)]; // [4, 8, 15, 16, 23, 42]

new Set(ids) constructs a set from the array (one entry per distinct value). The spread copies those entries into a fresh array in insertion order. It works for any mix of primitives:

javascript
const tags = ["js", "css", "js", "html", "css"];
[...new Set(tags)]; // ["js", "css", "html"]

If you only need to count the distinct values or test membership, you do not need the spread back to an array at all. new Set(ids).size is the count, and set.has(x) is an O(1) membership check, far cheaper than array.includes(x) in a loop.

Set membership semantics: NaN and signed zero

Set decides whether two values are the same using the SameValueZero algorithm. It behaves exactly like === with one deliberate exception, and two cases matter for deduping:

  • NaN deduplicates. This is the one place SameValueZero diverges from ===. Under ===, NaN === NaN is false, so a naive includes/indexOf dedupe leaves duplicate NaNs in. SameValueZero treats every NaN as the same value, so Set collapses them.
  • +0 and -0 collapse to one entry. This is not a special case, +0 === -0 is already true, so SameValueZero agrees and you get a single zero out. It is worth knowing because Object.is (SameValue) is the comparison that keeps the two zeros apart.
javascript
[...new Set([NaN, NaN])]; // [NaN]    one entry
[...new Set([0, -0])];    // [0]      one entry

This is the right behavior for almost every real dataset, and it is one more reason to prefer Set over a hand-rolled filter(indexOf) loop, which gets the NaN case wrong.

Dedupe an array of objects: the reference trap

This is the gotcha that sends people to the search box. Set compares objects by reference, so two object literals with identical contents are different values to it:

javascript
const users = [
  { id: 1, name: "Ada" },
  { id: 2, name: "Linus" },
  { id: 1, name: "Ada" },
];

[...new Set(users)].length; // 3   nothing was removed

Each { id: 1, name: "Ada" } is a separate object in memory, so Set keeps all three. The fix is to dedupe by a key you actually care about. A Map keyed on that property is the cleanest way: building the map overwrites earlier entries with the same key, and .values() gives you back one object per key.

javascript
const byId = [...new Map(users.map((u) => [u.id, u])).values()];
// [ { id: 1, name: "Ada" }, { id: 2, name: "Linus" } ]

Read it inside-out: users.map(u => [u.id, u]) builds [key, value] pairs, new Map(...) keeps only the last pair per key, and .values() spread back into an array gives the deduped result. Note this keeps the last object seen for a given id. To keep the first instead, reverse before building the map, or guard with if (!map.has(u.id)) map.set(u.id, u) in a loop.

If you need to dedupe by the whole object rather than one field, the usual trick is to key on a serialized form:

javascript
const seen = new Map(items.map((it) => [JSON.stringify(it), it]));
const unique = [...seen.values()];

That works, but mind the caveat: JSON.stringify is key-order sensitive, so {a: 1, b: 2} and {b: 2, a: 1} serialize differently and will not dedupe against each other. It also drops undefined values and functions. For a small, fixed object shape it is fine; for arbitrary objects, pin down a canonical key (often a real id) instead of stringifying.

I wrote a small te-prefixed helper for the by-property case, since it comes up constantly:

javascript
const teUniqueBy = (arr, keyFn) =>
  [...new Map(arr.map((item) => [keyFn(item), item])).values()];

teUniqueBy(users, (u) => u.id); // deduped by id

The ES5 fallback

Array.from(new Set(arr)) is exactly equivalent to [...new Set(arr)] and predates spread syntax. You only need it in environments without spread, which in practice means very old transpile targets:

javascript
var unique = Array.from(new Set(arr));

In 2026 there is no reason to prefer it for compatibility (Set and Array.from both date to ES2015, and spread arrived with them). Reach for Array.from when you also want to map in the same pass, for example Array.from(new Set(arr), (x) => x * 2).

[...new Set(arr)]. It wraps the array in a Set (which stores each value once) and spreads it back to an array, preserving first-seen order. This is the right answer for arrays of primitives: numbers, strings, booleans, null, undefined, symbols.

Because Set compares objects by reference, not by contents. Two object literals with the same fields are still two different objects in memory, so Set keeps both. Dedupe by a property instead with a Map: [...new Map(arr.map(o => [o.id, o])).values()].

Yes. A Set iterates in insertion order, so [...new Set(arr)] keeps the first occurrence of each value in its original position. The Map approach for objects keeps the last object seen per key (reverse first, or guard with has, if you want the first).

Set uses the SameValueZero comparison, so all NaN values count as one (unlike ===, where NaN !== NaN), and +0 and -0 collapse into a single entry. This is usually what you want, and it is something a filter(indexOf) loop gets wrong for NaN.

See also

Sources

Authoritative references this article was fact-checked against.

TagsJavaScriptarraysSetMapdedupeunique arrayremove duplicates

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Software Systems Architect · Senior Software Engineer · Engineering Leadership

Software systems architect and senior software engineer with more than two decades designing, building, and running production software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Now a CTO, though what I write here is drawn from the full arc of that work, across architecture, engineering, and operations, not any single job.

Keep reading

Related posts

Remove empty, null, false, or empty-string values from a PHP array. Covers array_filter, the '0 gets removed' gotcha, array_values re-indexing, multidimensional cleanup, and a performance comparison.

How to Remove Empty Values from an Array in PHP

Drop empty, null, or false values from a PHP array with array_filter and the right callback. Includes the '0 gets removed' gotcha, the array_values re-index pattern, multidimensional cleanup, and a performance comparison.

Merge two arrays in JavaScript: copy-merge with spread, merge in place with push, or copy with concat, and the choice by mutation, memory, and large-array safety.

How to Merge Two Arrays in JavaScript

Merge two arrays in JavaScript three ways: copy-merge with spread [...a, ...b], merge in place with a.push(...b), or copy with a.concat(b). Which to pick by mutation, memory, and the spread arg-count limit that bites on very large arrays.