Remove Duplicates from an Array of Objects in TypeScript

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
Command Prompt showing four TypeScript order objects reduced to three by keying a Map on the id property
Four rows in, three out: the repeated id collapses into one row.
Duplicates defined byKey to use
One property[o.id, o]
Two or more properties[`${o.city}|${o.customer}`, o]
The whole objecta stable JSON key with sorted properties
Text, ignoring case[o.email.toLowerCase(), o]
Keep the first duplicateonly set keys you have not seen
Keep the last duplicatea plain Map, which overwrites
Combine duplicatesread 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' ]
Command Prompt output showing a Map keeping the last duplicate with quantity five and the two alternatives keeping the first with quantity two
Same data, two answers: quantity 5 with a plain 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
Command Prompt output showing JSON stringify producing different keys for the same object written in a different property order and a stable key fixing it
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
Command Prompt output showing two duplicate order rows combined into one with their quantities added together
The repeated id keeps one row, and its quantity is 2 + 5 rather than either alone.

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);
Command Prompt output timing a Map keyed deduplication against filter with findIndex on fifty thousand objects
Same result, very different cost: one pass against a search inside a search.

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:

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.