How to Sort an Array of Objects by Property in TypeScript

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
Command Prompt showing a TypeScript file compiled with tsc and run with node, sorting an array of employee objects by age and by name
Compiled with 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' ]
Command Prompt output of TypeScript sort on numbers compared as strings, numeric ascending and descending sort, and strings sorted with localeCompare
Default sort vs a compare function for numbers and strings.

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' ]
Command Prompt output of a generic TypeScript sortBy function with keyof sorting employees by city ascending and by age descending
One helper for any property, ascending or descending.

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'.
Command Prompt showing tsc error TS2345 Argument of type salary is not assignable to parameter of type keyof Employee
A typo in the property name is a compile-time error, not a silent bad sort.

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' ]
Command Prompt output showing TypeScript sort mutating the original products array and toSorted returning a new sorted array
sort() returns the same array; toSorted() returns a new one.
GoalCompare 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 originalarr.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:

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.