To remove duplicates from an array of objects in TypeScript, key them with a Map: [...new Map(orders.map(o => [o.id, o])).values()] gives you one row per id, in the order they first appeared. A Set will not do it, because it compares objects by reference rather than by contents. This guide covers deduplicating by one property, by several, by the whole object, choosing whether the first or the last duplicate survives, merging duplicates instead of dropping them, and why the filter() with findIndex() version you often see is the slow way.
Every example was compiled and run with TypeScript 5.2.2 and Node.js v22.22.2 in the Windows Command Prompt, with tsc --target es2022 --lib es2023,dom --strict file.ts and then node file.js. The output is the real output. Reference: Map on MDN.
Remove duplicate objects by a property
The pattern is always the same: build a Map whose key is the thing that makes a row unique, then take its values. A Map keeps insertion order, so the result stays in the order your data arrived:
interface Order {
id: string;
customer: string;
city: string;
quantity: number;
}
const orders: Order[] = [
{ id: "ORD-1001", customer: "Emma Johnson", city: "Austin", quantity: 2 },
{ id: "ORD-1002", customer: "Michael Brown", city: "Denver", quantity: 1 },
{ id: "ORD-1001", customer: "Emma Johnson", city: "Austin", quantity: 5 }, // same id, later row
{ id: "ORD-1003", customer: "Olivia Davis", city: "Austin", quantity: 3 },
];
// one row per id, in the order they first appeared
const unique = [...new Map(orders.map(order => [order.id, order])).values()];
console.log(unique.map(o => `${o.id} qty ${o.quantity}`));
console.log(orders.length, "rows in,", unique.length, "rows out");
Output:
[ 'ORD-1001 qty 5', 'ORD-1002 qty 1', 'ORD-1003 qty 3' ]
4 rows in, 3 rows out
| Duplicates defined by | Key to use |
|---|---|
| One property | [o.id, o] |
| Two or more properties | [`${o.city}|${o.customer}`, o] |
| The whole object | a stable JSON key with sorted properties |
| Text, ignoring case | [o.email.toLowerCase(), o] |
| Keep the first duplicate | only set keys you have not seen |
| Keep the last duplicate | a plain Map, which overwrites |
| Combine duplicates | read the current value and merge into it |
Why new Set(objects) does not work
A Set removes duplicates by identity, and two objects with identical properties are still two different objects. It only drops repeats of the very same reference, as the middle line shows. Primitives are a different story, which is why it works for removing duplicates from an array of strings or numbers:
const first = { id: "ORD-1001", city: "Austin" };
const second = { id: "ORD-1001", city: "Austin" };
console.log(first === second); // same contents, different objects
console.log([...new Set([first, second, first])].length, "objects left");
console.log([...new Set(["a", "a", "b"])].length, "strings left");
Output:
false
2 objects left
2 strings left
Keep the first duplicate, or the last
A Map overwrites, so the last row with a given key wins. That is often wrong: the first row is usually the original and the later ones are the accidental repeats. Two ways to keep the first are below, and both preserve the original order. The second is worth knowing because it also works when you are filtering an array of objects in the same pass:
interface Order {
id: string;
customer: string;
city: string;
quantity: number;
}
const orders: Order[] = [
{ id: "ORD-1001", customer: "Emma Johnson", city: "Austin", quantity: 2 },
{ id: "ORD-1002", customer: "Michael Brown", city: "Denver", quantity: 1 },
{ id: "ORD-1001", customer: "Emma Johnson", city: "Austin", quantity: 5 }, // same id, later row
{ id: "ORD-1003", customer: "Olivia Davis", city: "Austin", quantity: 3 },
];
// a Map overwrites, so the LAST row with each id wins
const last = [...new Map(orders.map(o => [o.id, o])).values()];
console.log("last wins :", last.map(o => `${o.id}:${o.quantity}`));
// to keep the FIRST, only set a key that is not there yet
const seen = new Map<string, Order>();
for (const order of orders) {
if (!seen.has(order.id)) seen.set(order.id, order);
}
console.log("first wins:", [...seen.values()].map(o => `${o.id}:${o.quantity}`));
// the same idea with filter and a Set of ids already kept
const kept = new Set<string>();
const firstOnly = orders.filter(o => (kept.has(o.id) ? false : (kept.add(o.id), true)));
console.log("filter :", firstOnly.map(o => `${o.id}:${o.quantity}`));
Output:
last wins : [ 'ORD-1001:5', 'ORD-1002:1', 'ORD-1003:3' ]
first wins: [ 'ORD-1001:2', 'ORD-1002:1', 'ORD-1003:3' ]
filter : [ 'ORD-1001:2', 'ORD-1002:1', 'ORD-1003:3' ]
Map, quantity 2 when the first row wins.Duplicates defined by more than one property
When a row is only a duplicate if two fields match, join them into one key with a separator that cannot appear in the data. Normalise text keys first, otherwise Emma@Example.com and emma@example.com survive as two people. That normalising step is the same one collecting unique values needs:
interface Order {
id: string;
customer: string;
city: string;
quantity: number;
}
const orders: Order[] = [
{ id: "ORD-1001", customer: "Emma Johnson", city: "Austin", quantity: 2 },
{ id: "ORD-1002", customer: "Michael Brown", city: "Denver", quantity: 1 },
{ id: "ORD-1001", customer: "Emma Johnson", city: "Austin", quantity: 5 }, // same id, later row
{ id: "ORD-1003", customer: "Olivia Davis", city: "Austin", quantity: 3 },
];
// duplicates defined by two properties together
const byCustomerCity = [...new Map(orders.map(o => [`${o.customer}|${o.city}`, o])).values()];
console.log(byCustomerCity.map(o => `${o.customer} (${o.city})`));
// case-insensitive keys: normalise before using them
const emails = [
{ email: "Emma@Example.com", name: "Emma" },
{ email: "emma@example.com", name: "Emma J" },
{ email: "noah@example.com", name: "Noah" },
];
const byEmail = [...new Map(emails.map(e => [e.email.trim().toLowerCase(), e])).values()];
console.log(byEmail.map(e => `${e.email} -> ${e.name}`));
Output:
[
'Emma Johnson (Austin)',
'Michael Brown (Denver)',
'Olivia Davis (Austin)'
]
[ 'emma@example.com -> Emma J', 'noah@example.com -> Noah' ]
Removing duplicates by the whole object
If any two rows with identical contents count as duplicates, the key has to describe the entire object. JSON.stringify() nearly works, but its output depends on the order the properties were written in, so sort the keys first:
interface Point {
x: number;
y: number;
}
const points: Point[] = [
{ x: 1, y: 2 },
{ x: 3, y: 4 },
{ x: 1, y: 2 },
];
// JSON.stringify as the key works only while the properties are written in the same order
console.log(JSON.stringify({ x: 1, y: 2 }) === JSON.stringify({ y: 2, x: 1 }));
// a stable key sorts the property names first
const stableKey = (value: object): string =>
JSON.stringify(Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b))));
console.log(stableKey({ x: 1, y: 2 }) === stableKey({ y: 2, x: 1 }));
const unique = [...new Map(points.map(point => [stableKey(point), point])).values()];
console.log(unique, unique.length, "unique points");
Output:
false
true
[ { x: 1, y: 2 }, { x: 3, y: 4 } ] 2 unique points
false then true: sorting the property names makes the key stable.This is fine for flat objects. For nested data, write the key function around the fields that actually identify the row instead of trying to serialise everything.
Merge duplicates instead of dropping them
Often the duplicate rows each carry a number that should be added up rather than thrown away. Read what is already in the Map and merge into it, which is the same shape as merging arrays of objects by id:
interface Order {
id: string;
customer: string;
city: string;
quantity: number;
}
const orders: Order[] = [
{ id: "ORD-1001", customer: "Emma Johnson", city: "Austin", quantity: 2 },
{ id: "ORD-1002", customer: "Michael Brown", city: "Denver", quantity: 1 },
{ id: "ORD-1001", customer: "Emma Johnson", city: "Austin", quantity: 5 }, // same id, later row
{ id: "ORD-1003", customer: "Olivia Davis", city: "Austin", quantity: 3 },
];
// sometimes duplicates should be COMBINED rather than dropped
const totals = new Map<string, Order>();
for (const order of orders) {
const current = totals.get(order.id);
totals.set(order.id, current ? { ...current, quantity: current.quantity + order.quantity } : { ...order });
}
const merged = [...totals.values()];
console.log(merged.map(o => `${o.id} qty ${o.quantity}`));
console.log("total units:", merged.reduce((sum, o) => sum + o.quantity, 0));
Output:
[ 'ORD-1001 qty 7', 'ORD-1002 qty 1', 'ORD-1003 qty 3' ]
total units: 11
A typed helper
Two small functions cover everything above. K extends keyof T means the compiler rejects a property that does not exist, and the key-function version handles composite and normalised keys:
interface Order {
id: string;
customer: string;
city: string;
quantity: number;
}
const orders: Order[] = [
{ id: "ORD-1001", customer: "Emma Johnson", city: "Austin", quantity: 2 },
{ id: "ORD-1002", customer: "Michael Brown", city: "Denver", quantity: 1 },
{ id: "ORD-1001", customer: "Emma Johnson", city: "Austin", quantity: 5 }, // same id, later row
{ id: "ORD-1003", customer: "Olivia Davis", city: "Austin", quantity: 3 },
];
// by a property name, checked by the compiler
function dedupeBy<T extends object, K extends keyof T>(items: readonly T[], key: K): T[] {
return [...new Map(items.map(item => [item[key], item])).values()];
}
// by anything you can turn into a key
function dedupeWith<T>(items: readonly T[], keyOf: (item: T) => string): T[] {
return [...new Map(items.map(item => [keyOf(item), item])).values()];
}
console.log(dedupeBy(orders, "id").length, "by id");
console.log(dedupeBy(orders, "city").map(o => o.city), "by city");
console.log(dedupeWith(orders, o => `${o.city}|${o.customer}`).length, "by city and customer");
Output:
3 by id
[ 'Austin', 'Denver' ] by city
3 by city and customer
Why not filter() with findIndex()?
Because it searches the whole array for every element. It reads nicely on five rows and falls apart on fifty thousand:
interface Row {
id: number;
name: string;
}
const rows: Row[] = Array.from({ length: 50_000 }, (_, i) => ({ id: i % 25_000, name: `Row ${i}` }));
function time(label: string, fn: () => number): void {
fn(); // warm-up run
const start = performance.now();
const kept = fn();
console.log(label.padEnd(30), (performance.now() - start).toFixed(1).padStart(8), "ms", "kept", kept);
}
time("Map by id", () => [...new Map(rows.map(r => [r.id, r])).values()].length);
time("filter + findIndex", () =>
rows.filter((row, index) => rows.findIndex(other => other.id === row.id) === index).length);
Use the Map. It is shorter, it keeps the order, and it does not slow down as the data grows, the same reason a Set beats includes() when filtering by several properties.
More TypeScript array guides worth reading:
- Get unique values from an array of objects
- Remove duplicates from an array
- Arrays of objects in TypeScript
- Merge arrays of objects
- Remove an item from an array
Frequently asked questions
How do I remove duplicates from an array of objects in TypeScript?
Key them with a Map: [...new Map(items.map(i => [i.id, i])).values()]. One row survives per key, in first-seen order.
Why does new Set() not remove my duplicate objects?
Set compares objects by reference, so two objects with the same properties are two values. Use a Map keyed on a property.
Does the Map version keep the first or the last duplicate?
The last, because later entries overwrite earlier ones. To keep the first, only set keys that are not in the Map yet.
How do I remove duplicates based on two properties?
Join them into one key: `${o.city}|${o.customer}`, using a separator that cannot appear in the values.
How do I remove duplicates by the whole object?
Build a key from the sorted properties, because JSON.stringify() gives different output when the properties are in a different order.
How do I combine duplicates instead of removing them?
Read the existing entry from the Map and merge the new row into it, for example adding the quantities together.
Is filter() with findIndex() a good way to deduplicate?
It works but it scans the array for every element. A Map does the same job in one pass and stays fast on large arrays.
Bijay Kumar is a 13-time Microsoft MVP with more than 18 years in software development, and the founder of Python Guides and TSinfo Technologies. He started out building .NET and SharePoint solutions at HP, TCS and KPIT before moving into Python, machine learning and AI, and he also builds web apps with TypeScript and React. He writes the tutorials here himself, and every example is run before publishing so you see the real output. More about Bijay · Microsoft MVP profile · LinkedIn