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' ]
| What you want | Use |
|---|---|
| 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 properties | a joined key: `${i.state}|${i.city}` |
| Distinct values with counts | a Map<string, number> and a loop |
| Whole objects deduplicated | see 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
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' ]
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' ]
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);
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:
- Remove duplicates from an array
- Remove duplicate objects from an array
- Filter an array of objects
- Sort arrays in TypeScript
- Merge arrays of objects
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.
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