When API data contains repeated records, you may want to keep one object per ID without changing the order of the remaining records. This tutorial shows how to remove duplicate objects from an array in JavaScript with Set and Map, and how to choose a policy for records with missing IDs.
Key Takeaways
- Objects with the same fields are not necessarily equal in JavaScript: separate objects are compared by reference.
- To keep the first record for each ID and preserve its position, track seen IDs with a
Set. - To keep the last record for each ID while preserving the order of those last occurrences, record each ID’s final index with a
Map. - Decide explicitly what to do with missing IDs. The examples below keep those records rather than treating all of them as duplicates.
- The examples are written to be runnable, but I could not execute them in this environment.
Why object equality does not remove duplicate records
Consider two API records with identical fields:
const first = { id: 7, name: "Mira" };
const second = { id: 7, name: "Mira" };
console.log(first === second); // false
Each object is a separate value in memory, so JavaScript’s strict equality comparison checks whether the references point to the same object—not whether their properties match. A Set of objects therefore removes repeated references, but not separate records that happen to have the same ID:
const records = [first, second];
console.log(new Set(records).size); // 2
For API records, deduplicate by the field that defines identity, such as id.
Keep the first record with a Set
Use a Set to track IDs already encountered. Filtering the original array keeps the first record for each ID and leaves the retained records in their original order.
function uniqueByFirstId(records) {
const seenIds = new Set();
return records.filter((record) => {
// Keep nullish entries and records without an ID; do not merge them.
if (record == null || record.id == null) {
return true;
}
if (seenIds.has(record.id)) {
return false;
}
seenIds.add(record.id);
return true;
});
}
const records = [
{ id: 1, name: "Asha" },
{ id: 2, name: "Ben" },
{ id: 1, name: "Asha (updated)" },
{ name: "No ID" },
{ name: "Also no ID" },
];
console.log(uniqueByFirstId(records));
// [
// { id: 1, name: "Asha" },
// { id: 2, name: "Ben" },
// { name: "No ID" },
// { name: "Also no ID" }
// ]
This policy keeps the first version of an ID and discards later versions. The check record.id == null intentionally treats both null and undefined as missing, while still accepting values such as 0 and an empty string. If an empty string is not a valid ID in your API, add a validation rule for it.
The code assumes each non-null record is an object with an id property. If input can contain other types, validate or normalize those values before deduplicating.
Keep the last record with a Map
If later API records should replace earlier ones, keep the record at each ID’s last occurrence. A first pass stores the final index for each ID in a Map. A second pass filters the original array by those indexes. This preserves the order of the retained last occurrences and leaves records without IDs in their original positions.
function uniqueByLastId(records) {
const lastIndexById = new Map();
records.forEach((record, index) => {
if (record != null && record.id != null) {
lastIndexById.set(record.id, index);
}
});
return records.filter((record, index) => {
if (record == null || record.id == null) {
return true;
}
return lastIndexById.get(record.id) === index;
});
}
const records = [
{ id: 1, name: "Asha" },
{ id: 2, name: "Ben" },
{ id: 1, name: "Asha (updated)" },
];
console.log(uniqueByLastId(records));
// [
// { id: 2, name: "Ben" },
// { id: 1, name: "Asha (updated)" }
// ]
The result follows the positions of the last occurrences: ID 2 appeared last at index 1, and ID 1 appeared last at index 2. If instead you want the last value for each ID but the order of each ID’s first appearance, you can build a Map by inserting each record in sequence:
const lastValueById = new Map();
for (const record of records) {
if (record != null && record.id != null) {
lastValueById.set(record.id, record);
}
}
console.log([...lastValueById.values()]);
Updating an existing key in a Map replaces its value without moving that key’s original insertion position. This compact version omits records without IDs, so use the earlier function when those records must be retained.
Choose a policy for missing IDs
Missing IDs cannot reliably identify duplicate records. In the examples, each record with a missing or null ID is preserved. That is a conservative choice: it avoids accidentally collapsing unrelated records into one.
Other valid policies depend on your data:
- Reject invalid records: useful when the API contract requires every record to have an ID.
- Keep all missing-ID records: useful when they may represent distinct data, as in the examples.
- Use another stable key: appropriate only if the API provides a field or combination of fields that uniquely identifies the record.
Avoid using a fallback such as record.id ?? "missing" as a deduplication key. It assigns the same key to every record without an ID, so only one would survive.
Common Mistakes
- Putting objects directly in a
Set: this only deduplicates repeated references, not separate objects with matching IDs. - Using
if (!record.id)to detect missing IDs: this incorrectly treats valid falsy IDs, such as0, as absent. - Letting the input order decide the winner accidentally: filtering with a
Setkeeps the first record; repeatedly setting aMapvalue keeps the last. Choose deliberately. - Assuming every
Map-based result has the order you expect: replacing a key’s value does not move its insertion position. Use the last-index approach when the order of last occurrences matters. - Collapsing all missing IDs together: give missing IDs an explicit policy rather than using one shared fallback key.
- Comparing only one field when it is not unique: deduplication is only as reliable as the field chosen to represent record identity.
Best Practices
- Confirm which field the API guarantees as a unique identifier.
- Decide whether the first or last version of a duplicate record should win.
- Define how to handle
null,undefined, and invalid IDs before processing production data. - Test representative cases: repeated IDs, distinct IDs,
id: 0, missing IDs, and records whose fields differ despite sharing an ID. - Keep deduplication close to the point where the API data is normalized, and document the chosen winner and ordering policy.
Conclusion
To remove duplicate objects from a JavaScript array, compare the records by a stable ID rather than by object reference. Use a Set when the first occurrence should win; use a Map-based last-index approach when the last occurrence should win and its position should determine the result order. Make missing-ID behavior explicit so distinct records are not discarded by accident.
References
- MDN:
Set— reference for storing unique values and the equality behavior used bySet. - MDN:
Map— reference for key-value collections and insertion order. - MDN:
Array.prototype.filter()— reference for creating a filtered array.
