To sort an array of objects by property in TypeScript, pass a compare function to sort(): arr.sort((a, b) => a.age - b.age) for a number property and arr.sort((a, b) => a.name.localeCompare(b.name)) for a string property. Swap a and b for descending order. This guide covers the TypeScript array sort for numbers, strings and dates, sorting by multiple properties, a type-safe generic sortBy function, optional values, and why sort() changes your array (and how toSorted() avoids that).
Every example was compiled with TypeScript 5.2.2 and Node.js v22.22.2 in the Windows Command Prompt, using tsc --target es2022 --lib es2023,dom --strict file.ts and node file.js. Reference: Array.prototype.sort() on MDN and Generics in the TypeScript Handbook.
Sort an array of objects by a property
The compare function gets two elements and returns a negative number if a should come first, a positive number if b should come first, and 0 if they are equal. For numbers, a.age - b.age does exactly that; for strings, localeCompare() returns the same kind of number:
interface Employee {
name: string;
age: number;
city: string;
}
const employees: Employee[] = [
{ name: "Olivia", age: 34, city: "Denver" },
{ name: "liam", age: 28, city: "Austin" },
{ name: "Emma", age: 41, city: "Boston" },
{ name: "Noah", age: 28, city: "Denver" },
];
// by a number property: subtract the values
const byAge = [...employees].sort((a, b) => a.age - b.age);
console.log(byAge.map(e => `${e.name} (${e.age})`).join(", "));
// by a string property: localeCompare
const byName = [...employees].sort((a, b) => a.name.localeCompare(b.name));
console.log(byName.map(e => e.name).join(", "));
Output:
liam (28), Noah (28), Olivia (34), Emma (41)
Emma, liam, Noah, Olivia
tsc, run with node: sorted by age, then by name.Notice that "liam" is sorted as “Liam” would be: localeCompare() puts letters in dictionary order, whatever their case. The copy [...employees] keeps the original array untouched (more on that below).
TypeScript array sort without a compare function
Called with no argument, sort() converts every element to a string and compares UTF-16 code units. That is fine for simple lowercase strings, but wrong for numbers and for mixed-case or accented text:
const scores: number[] = [100, 25, 9, 1000, 3];
console.log([...scores].sort()); // compares as strings!
console.log([...scores].sort((a, b) => a - b)); // numeric, ascending
console.log([...scores].sort((a, b) => b - a)); // numeric, descending
const names: string[] = ["banana", "Cherry", "apple", "Äpfel"];
console.log([...names].sort()); // code point order
console.log([...names].sort((a, b) => a.localeCompare(b))); // human order
Output:
[ 100, 1000, 25, 3, 9 ]
[ 3, 9, 25, 100, 1000 ]
[ 1000, 100, 25, 9, 3 ]
[ 'Cherry', 'apple', 'banana', 'Äpfel' ]
[ 'Äpfel', 'apple', 'banana', 'Cherry' ]
Sort descending and by multiple properties
For descending order, subtract the other way round. To sort by a second property when the first is equal, chain the comparisons with ||: a result of 0 is falsy, so the second comparison is used only for ties:
interface Employee {
name: string;
age: number;
city: string;
}
const employees: Employee[] = [
{ name: "Olivia", age: 34, city: "Denver" },
{ name: "liam", age: 28, city: "Austin" },
{ name: "Emma", age: 41, city: "Boston" },
{ name: "Noah", age: 28, city: "Denver" },
];
// descending: swap a and b
const oldestFirst = [...employees].sort((a, b) => b.age - a.age);
console.log(oldestFirst.map(e => `${e.name} ${e.age}`));
// two properties: age ascending, then name A-Z for equal ages
const byAgeThenName = [...employees].sort(
(a, b) => a.age - b.age || a.name.localeCompare(b.name, undefined, { sensitivity: "base" })
);
console.log(byAgeThenName.map(e => `${e.age} ${e.name}`));
Output:
[ 'Emma 41', 'Olivia 34', 'liam 28', 'Noah 28' ]
[ '28 liam', '28 Noah', '34 Olivia', '41 Emma' ]
A type-safe generic sortBy function
If you sort by different properties in many places, a generic helper keeps the code short. K extends keyof T means only real property names of the objects are accepted:
interface Employee {
name: string;
age: number;
city: string;
}
const employees: Employee[] = [
{ name: "Olivia", age: 34, city: "Denver" },
{ name: "liam", age: 28, city: "Austin" },
{ name: "Emma", age: 41, city: "Boston" },
{ name: "Noah", age: 28, city: "Denver" },
];
type Order = "asc" | "desc";
function sortBy<T, K extends keyof T>(items: readonly T[], key: K, order: Order = "asc"): T[] {
const dir = order === "asc" ? 1 : -1;
return [...items].sort((a, b) => {
const x = a[key], y = b[key];
if (typeof x === "string" && typeof y === "string") return x.localeCompare(y) * dir;
return (x < y ? -1 : x > y ? 1 : 0) * dir;
});
}
console.log(sortBy(employees, "city").map(e => `${e.city}/${e.name}`));
console.log(sortBy(employees, "age", "desc").map(e => `${e.age}/${e.name}`));
Output:
[ 'Austin/liam', 'Boston/Emma', 'Denver/Olivia', 'Denver/Noah' ]
[ '41/Emma', '34/Olivia', '28/liam', '28/Noah' ]
Try to sort by a property that does not exist and the compiler stops you before the code ever runs:
interface Employee {
name: string;
age: number;
city: string;
}
const employees: Employee[] = [
{ name: "Olivia", age: 34, city: "Denver" },
{ name: "liam", age: 28, city: "Austin" },
{ name: "Emma", age: 41, city: "Boston" },
{ name: "Noah", age: 28, city: "Denver" },
];
type Order = "asc" | "desc";
function sortBy<T, K extends keyof T>(items: readonly T[], key: K, order: Order = "asc"): T[] {
const dir = order === "asc" ? 1 : -1;
return [...items].sort((a, b) => {
const x = a[key], y = b[key];
if (typeof x === "string" && typeof y === "string") return x.localeCompare(y) * dir;
return (x < y ? -1 : x > y ? 1 : 0) * dir;
});
}
console.log(sortBy(employees, "city").map(e => `${e.city}/${e.name}`));
console.log(sortBy(employees, "age", "desc").map(e => `${e.age}/${e.name}`));
console.log(sortBy(employees, "salary"));
tsc output:
sort_by_error.ts(27,31): error TS2345: Argument of type '"salary"' is not assignable to parameter of type 'keyof Employee'.
Sort by a date property and handle optional values
Subtracting two Date objects is a type error in TypeScript, so compare getTime() values. For optional properties, decide where undefined should go and handle it first:
interface Order {
id: number;
placed: Date;
shipped?: string; // optional property
}
const orders: Order[] = [
{ id: 1, placed: new Date("2025-03-14"), shipped: "2025-03-16" },
{ id: 2, placed: new Date("2024-11-02") },
{ id: 3, placed: new Date("2025-01-20"), shipped: "2025-01-21" },
];
// Date objects: compare getTime() numbers
const newest = [...orders].sort((a, b) => b.placed.getTime() - a.placed.getTime());
console.log(newest.map(o => `#${o.id} ${o.placed.toISOString().slice(0, 10)}`));
// optional values: put undefined last
const byShipped = [...orders].sort((a, b) =>
a.shipped === undefined ? 1 : b.shipped === undefined ? -1 : a.shipped.localeCompare(b.shipped)
);
console.log(byShipped.map(o => `#${o.id} ${o.shipped ?? "not shipped"}`));
Output:
[ '#1 2025-03-14', '#3 2025-01-20', '#2 2024-11-02' ]
[ '#3 2025-01-21', '#1 2025-03-16', '#2 not shipped' ]
sort() changes the original array: use toSorted() or a copy
sort() sorts in place and returns the same array. If other code still uses the original order (React state, for example), sort a copy with [...arr].sort() or use toSorted(), which needs "lib": ["es2023", "dom"] (or a newer target) in tsconfig.json and Node.js 20 or later:
const products = [
{ sku: "B-200", price: 19.99 },
{ sku: "A-100", price: 4.5 },
{ sku: "C-300", price: 12 },
];
const sorted = products.sort((a, b) => a.price - b.price);
console.log(sorted === products, products.map(p => p.sku)); // sort() changed the original
const items = [{ sku: "B-200" }, { sku: "A-100" }, { sku: "C-300" }];
const copy = items.toSorted((a, b) => a.sku.localeCompare(b.sku)); // ES2023, Node 20+
console.log(copy.map(p => p.sku), items.map(p => p.sku)); // original unchanged
Output:
true [ 'A-100', 'C-300', 'B-200' ]
[ 'A-100', 'B-200', 'C-300' ] [ 'B-200', 'A-100', 'C-300' ]
sort() returns the same array; toSorted() returns a new one.| Goal | Compare function |
|---|---|
| Number property, ascending | (a, b) => a.age - b.age |
| Number property, descending | (a, b) => b.age - a.age |
| String property | (a, b) => a.name.localeCompare(b.name) |
| Date property | (a, b) => a.date.getTime() - b.date.getTime() |
| Two properties | (a, b) => a.age - b.age || a.name.localeCompare(b.name) |
| Keep the original | arr.toSorted(fn) or [...arr].sort(fn) |
Since ES2019 the sort is stable: objects that compare as equal keep their original order, which is why sorting in two passes also works.
You might also like these TypeScript array tutorials:
- Sort arrays in TypeScript
- Filter an array of objects in TypeScript
- Find an object in a TypeScript array
- Remove duplicates from an array of objects
- Find the maximum value in an array of objects
Frequently asked questions
How do I sort an array of objects by property in TypeScript?
Pass a compare function to sort(): arr.sort((a, b) => a.price - b.price) for numbers or arr.sort((a, b) => a.name.localeCompare(b.name)) for strings.
How do I sort in descending order?
Swap the operands: (a, b) => b.price - a.price, or b.name.localeCompare(a.name) for strings.
How do I sort by two properties?
Chain the comparisons with ||: (a, b) => a.age - b.age || a.name.localeCompare(b.name).
Why does sort() put 100 before 25?
Without a compare function, sort() compares elements as strings. Use (a, b) => a - b for numbers.
Does sort() change the original array?
Yes, it sorts in place. Use [...arr].sort(fn) or arr.toSorted(fn) (ES2023) to get a new array.
How do I make a sort function that only accepts valid property names?
Use a generic with keyof: function sortBy<T, K extends keyof T>(items: T[], key: K). A wrong key is a compile error.
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