Get Unique Values from an Array of Objects in TypeScript

To get unique values from an array of objects in TypeScript, map the property you want and wrap it in a Set: [...new Set(orders.map(order => order.city))] gives you every distinct city, in the order they first appeared. That works because a Set compares primitives by value. Objects are a different story: a Set compares them by reference, so two identical-looking objects both survive. This guide covers distinct values of a property, distinct objects by a key, composite keys, counting each distinct value, a typed generic helper, and the performance difference that matters once the array is long.

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: Set on MDN.

Get unique values from an array of objects

Two steps: map() to pull the property out, and a Set to drop the repeats. Spreading the Set back into an array gives you something you can sort, render or send on:

interface Order {
  id: string;
  customer: string;
  city: string;
  state: string;
  total: number;
}

const orders: Order[] = [
  { id: "ORD-1001", customer: "Emma Johnson", city: "Austin", state: "TX", total: 249.99 },
  { id: "ORD-1002", customer: "Michael Brown", city: "Denver", state: "CO", total: 89.5 },
  { id: "ORD-1003", customer: "Emma Johnson", city: "Austin", state: "TX", total: 430 },
  { id: "ORD-1004", customer: "Olivia Davis", city: "Seattle", state: "WA", total: 15 },
  { id: "ORD-1005", customer: "Michael Brown", city: "Austin", state: "TX", total: 60 },
];

// pull out one property, then let a Set drop the repeats
const cities: string[] = [...new Set(orders.map(order => order.city))];
console.log(cities);

const customers = [...new Set(orders.map(order => order.customer))];
console.log(customers, customers.length, "distinct customers");

// Array.from does the same thing and can map at the same time
console.log(Array.from(new Set(orders.map(o => o.state))).sort());

Output:

[ 'Austin', 'Denver', 'Seattle' ]
[ 'Emma Johnson', 'Michael Brown', 'Olivia Davis' ] 3 distinct customers
[ 'CO', 'TX', 'WA' ]
Command Prompt output showing distinct cities, distinct customers and sorted distinct states pulled from an array of order objects in TypeScript
Five orders, three distinct cities, three distinct customers, in first-seen order.
What you wantUse
Distinct values of one property[...new Set(items.map(i => i.city))]
Sorted distinct values[...new Set(items.map(i => i.city))].sort()
One object per key value[...new Map(items.map(i => [i.city, i])).values()]
Distinct pairs of propertiesa joined key: `${i.state}|${i.city}`
Distinct values with countsa Map<string, number> and a loop
Whole objects deduplicatedsee the guide on removing duplicate objects

Why a Set does not remove duplicate objects

This is the part that surprises people. A Set uses the same equality as ===: two strings with the same characters are one value, but two objects with the same properties are two different references. So a Set of objects only removes the ones that are literally the same object. To deduplicate objects by their contents, key them with a Map, as the next section does, or see removing duplicates from an array of objects:

const first = { city: "Austin", state: "TX" };
const second = { city: "Austin", state: "TX" };

console.log(first === second);                       // different objects, same contents

const objects = [first, second, first];
console.log([...new Set(objects)].length, "objects survive a Set");   // only the exact duplicate goes

const cities = ["Austin", "Austin", "Denver"];
console.log([...new Set(cities)].length, "strings survive a Set");    // primitives compare by value

Output:

false
2 objects survive a Set
2 strings survive a Set
Command Prompt output showing that a Set keeps two objects with identical contents while it removes duplicate strings
Three objects go in, two come out: only the repeated reference was removed.

One object per property value

A Map keyed on the property does what the Set cannot. Because later entries overwrite earlier ones, the plain version keeps the last object for each key; reverse the input if you want the first. This is the same pattern as merging arrays of objects by id:

interface Order {
  id: string;
  customer: string;
  city: string;
  state: string;
  total: number;
}

const orders: Order[] = [
  { id: "ORD-1001", customer: "Emma Johnson", city: "Austin", state: "TX", total: 249.99 },
  { id: "ORD-1002", customer: "Michael Brown", city: "Denver", state: "CO", total: 89.5 },
  { id: "ORD-1003", customer: "Emma Johnson", city: "Austin", state: "TX", total: 430 },
  { id: "ORD-1004", customer: "Olivia Davis", city: "Seattle", state: "WA", total: 15 },
  { id: "ORD-1005", customer: "Michael Brown", city: "Austin", state: "TX", total: 60 },
];

// one object per customer, keeping the LAST one seen
const lastPerCustomer = [...new Map(orders.map(order => [order.customer, order])).values()];
console.log(lastPerCustomer.map(o => `${o.customer} ${o.id}`));

// keeping the FIRST one instead: reverse the input, or check before setting
const firstPerCustomer = [...new Map([...orders].reverse().map(o => [o.customer, o])).values()].reverse();
console.log(firstPerCustomer.map(o => `${o.customer} ${o.id}`));

Output:

[
  'Emma Johnson ORD-1003',
  'Michael Brown ORD-1005',
  'Olivia Davis ORD-1004'
]
[
  'Emma Johnson ORD-1001',
  'Olivia Davis ORD-1004',
  'Michael Brown ORD-1002'
]

Distinct combinations of two properties

For “one row per city and state”, build a key out of both values. A joined string with a separator that cannot appear in the data is the simplest option. JSON.stringify() also works, but only if the properties are always written in the same order, which the last two lines show:

interface Order {
  id: string;
  customer: string;
  city: string;
  state: string;
  total: number;
}

const orders: Order[] = [
  { id: "ORD-1001", customer: "Emma Johnson", city: "Austin", state: "TX", total: 249.99 },
  { id: "ORD-1002", customer: "Michael Brown", city: "Denver", state: "CO", total: 89.5 },
  { id: "ORD-1003", customer: "Emma Johnson", city: "Austin", state: "TX", total: 430 },
  { id: "ORD-1004", customer: "Olivia Davis", city: "Seattle", state: "WA", total: 15 },
  { id: "ORD-1005", customer: "Michael Brown", city: "Austin", state: "TX", total: 60 },
];

// distinct city + state pairs, using a joined key
const pairs = [...new Map(orders.map(o => [`${o.state}|${o.city}`, { city: o.city, state: o.state }])).values()];
console.log(pairs.map(p => `${p.city}, ${p.state}`));

// JSON.stringify also works as a key, but property ORDER matters
const a = JSON.stringify({ city: "Austin", state: "TX" });
const b = JSON.stringify({ state: "TX", city: "Austin" });
console.log(a === b, a, b);

Output:

[ 'Austin, TX', 'Denver, CO', 'Seattle, WA' ]
false {"city":"Austin","state":"TX"} {"state":"TX","city":"Austin"}

Distinct values with a count

Nine times out of ten the next question after “which cities?” is “how many from each?”. One Map and one loop answers both, and swapping the + 1 for a total turns it into revenue per city. It is the same shape as a group-by, and it walks the array only once:

interface Order {
  id: string;
  customer: string;
  city: string;
  state: string;
  total: number;
}

const orders: Order[] = [
  { id: "ORD-1001", customer: "Emma Johnson", city: "Austin", state: "TX", total: 249.99 },
  { id: "ORD-1002", customer: "Michael Brown", city: "Denver", state: "CO", total: 89.5 },
  { id: "ORD-1003", customer: "Emma Johnson", city: "Austin", state: "TX", total: 430 },
  { id: "ORD-1004", customer: "Olivia Davis", city: "Seattle", state: "WA", total: 15 },
  { id: "ORD-1005", customer: "Michael Brown", city: "Austin", state: "TX", total: 60 },
];

// distinct values with how often each one appears
const perCity = new Map<string, number>();
for (const order of orders) {
  perCity.set(order.city, (perCity.get(order.city) ?? 0) + 1);
}
console.log([...perCity]);

// and the total per city, which is the same loop with a different sum
const revenue = new Map<string, number>();
for (const order of orders) {
  revenue.set(order.city, (revenue.get(order.city) ?? 0) + order.total);
}
console.log([...revenue].map(([city, total]) => `${city}: $${total.toFixed(2)}`));

Output:

[ [ 'Austin', 3 ], [ 'Denver', 1 ], [ 'Seattle', 1 ] ]
[ 'Austin: $739.99', 'Denver: $89.50', 'Seattle: $15.00' ]
Command Prompt output of a Map holding each distinct city with the number of orders and the total revenue for that city
Distinct cities with their order counts, then the same loop summing revenue.

A typed helper for any property

With K extends keyof T the compiler checks the property name and gives the result the right type: uniqueValues(orders, "city") is a string[], and uniqueValues(orders, "total") is a number[]. Two small functions cover nearly every case, and they pair with filtering an array of objects:

interface Order {
  id: string;
  customer: string;
  city: string;
  state: string;
  total: number;
}

const orders: Order[] = [
  { id: "ORD-1001", customer: "Emma Johnson", city: "Austin", state: "TX", total: 249.99 },
  { id: "ORD-1002", customer: "Michael Brown", city: "Denver", state: "CO", total: 89.5 },
  { id: "ORD-1003", customer: "Emma Johnson", city: "Austin", state: "TX", total: 430 },
  { id: "ORD-1004", customer: "Olivia Davis", city: "Seattle", state: "WA", total: 15 },
  { id: "ORD-1005", customer: "Michael Brown", city: "Austin", state: "TX", total: 60 },
];

// distinct values of any property, typed: uniqueValues(orders, "city") is string[]
function uniqueValues<T, K extends keyof T>(items: readonly T[], key: K): T[K][] {
  return [...new Set(items.map(item => item[key]))];
}

const cities: string[] = uniqueValues(orders, "city");
const totals: number[] = uniqueValues(orders, "total");
console.log(cities, totals.length, "distinct totals");

// and the same idea returning whole objects, one per key value
function uniqueBy<T, K extends keyof T>(items: readonly T[], key: K): T[] {
  return [...new Map(items.map(item => [item[key], item])).values()];
}

console.log(uniqueBy(orders, "city").map(o => `${o.city} ${o.id}`));

Output:

[ 'Austin', 'Denver', 'Seattle' ] 5 distinct totals
[ 'Austin ORD-1005', 'Denver ORD-1002', 'Seattle ORD-1004' ]

Four things that catch people out

Set equality is not quite ===. It uses SameValueZero, so NaN counts as equal to itself even though NaN === NaN is false, and 0 and -0 are one value. Beyond that: null and undefined are ordinary values and will appear in your results, and passing a string to new Set() splits it into characters:

const numbers = [1, NaN, NaN, 0, -0, 2];
const notANumber: number = NaN;

console.log(notANumber === notANumber);               // NaN is not equal to itself
console.log([...new Set(numbers)]);                    // yet a Set keeps ONE NaN, and 0 and -0 are one value

const messy: (string | null | undefined)[] = ["Austin", null, "Austin", undefined, null];
console.log([...new Set(messy)]);                      // null and undefined are values too

console.log([...new Set("Mississippi")].join(""));     // a string is iterable: characters, not words

const mixed = [1, "1", 1];
console.log([...new Set(mixed)]);                      // 1 and "1" are different values

Output:

false
[ 1, NaN, 0, 2 ]
[ 'Austin', null, undefined ]
Misp
[ 1, '1' ]
Command Prompt output showing a Set keeping one NaN, treating zero and minus zero as one value, and splitting a string into unique characters
One NaN survives, -0 merges with 0, and "Mississippi" becomes its letters.

If the array holds optional values, strip them first with a type predicate, as in removing empty strings from an array, so the result is a clean string[].

Set, or filter and indexOf?

You will still see filter((value, index) => arr.indexOf(value) === index) in tutorials. It works, but it searches the whole array for every element, so the cost grows with the square of the length. Over 50,000 rows:

const rows: string[] = Array.from({ length: 50_000 }, (_, i) => `city-${i % 5_000}`);

function time(label: string, fn: () => number): void {
  fn();                                   // warm-up run
  const start = performance.now();
  const count = fn();
  console.log(label.padEnd(26), (performance.now() - start).toFixed(1).padStart(8), "ms", "unique", count);
}

time("Set", () => [...new Set(rows)].length);
time("filter + indexOf", () => rows.filter((value, index) => rows.indexOf(value) === index).length);
time("reduce + includes", () => rows.reduce<string[]>((acc, value) => (acc.includes(value) ? acc : [...acc, value]), []).length);
Command Prompt output timing a Set against filter with indexOf and reduce with includes for finding unique values in fifty thousand rows
The Set finishes in milliseconds; the other two do the same job the slow way.

Use a Set. The one-liner is shorter and faster, and the reduce version that builds a new array on every element is the worst of the three.

More TypeScript array guides worth reading:

Frequently asked questions

How do I get unique values from an array of objects in TypeScript?

Map the property and wrap it in a Set: [...new Set(items.map(i => i.city))]. The result keeps the order values first appeared in.

Why does a Set not remove my duplicate objects?

Because it compares objects by reference. Two objects with identical properties are still two values. Key them with a Map instead.

How do I get one object per property value?

[...new Map(items.map(i => [i.city, i])).values()]. Later entries win, so reverse the array first if you want the first match.

How do I find unique combinations of two properties?

Build a composite key, such as `${i.state}|${i.city}`, and use it as the Map key.

How do I count how many times each value appears?

Loop once into a Map<string, number>, adding 1 to the current value for each item.

Is filter() with indexOf slower than a Set?

Much slower on large arrays: it scans the array for every element. A Set is a single pass and the shorter code.

Does Set treat NaN as a duplicate?

Yes. Set uses SameValueZero, so one NaN survives, and 0 and -0 are kept as one value.