How to Sort Arrays in TypeScript

To sort an array in TypeScript, call sort() with a compare function: numbers.sort((a, b) => a - b) for numbers and names.sort((a, b) => a.localeCompare(b)) for text. Swap a and b for descending order. Without a compare function sort() turns every element into a string first, which is why [100, 25, 9, 1000, 3] comes back as [100, 1000, 25, 3, 9]. It also sorts the array in place, so use toSorted() or a copy when the original matters. Everything below is compiled and run, including the sorting rules that are easy to get wrong.

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: Array.prototype.sort() on MDN.

Sort arrays in TypeScript

The compare function receives two elements and returns a negative number if a comes first, a positive number if b does, and 0 when they tie. That is all there is to it:

const prices: number[] = [100, 25, 9, 1000, 3];

console.log([...prices].sort());                 // default: compared as TEXT
console.log([...prices].sort((a, b) => a - b));  // ascending
console.log([...prices].sort((a, b) => b - a));  // descending

const cities: string[] = ["Seattle", "austin", "Denver"];
console.log([...cities].sort());                                  // uppercase first
console.log([...cities].sort((a, b) => a.localeCompare(b)));      // dictionary order

Output:

[ 100, 1000, 25, 3, 9 ]
[ 3, 9, 25, 100, 1000 ]
[ 1000, 100, 25, 9, 3 ]
[ 'Denver', 'Seattle', 'austin' ]
[ 'austin', 'Denver', 'Seattle' ]
Command Prompt showing a TypeScript file compiled with tsc and run with node, sorting numbers with and without a compare function and strings with localeCompare
Without a compare function, 1000 lands between 100 and 25.
What you are sortingCompare function
Numbers, ascending(a, b) => a - b
Numbers, descending(a, b) => b - a
Text(a, b) => a.localeCompare(b)
Text, ignoring case(a, b) => a.localeCompare(b, undefined, { sensitivity: "base" })
Text with numbers in it(a, b) => a.localeCompare(b, undefined, { numeric: true })
Dates(a, b) => a.date.getTime() - b.date.getTime()
Two keys at once(a, b) => a.team.localeCompare(b.team) || a.name.localeCompare(b.name)

Sort numbers in TypeScript

Subtraction gives the comparator exactly the negative, zero or positive number it expects, and it handles negatives and decimals without any extra work:

const temperatures: number[] = [68, -4, 102, 0, 55.5];

console.log([...temperatures].sort((a, b) => a - b));
console.log([...temperatures].sort((a, b) => b - a));

const scores: (number | undefined)[] = [88, undefined, 95, 72];
console.log([...scores].sort((a, b) => (a ?? 0) - (b ?? 0)));   // undefined ignores the comparator

Output:

[ -4, 0, 55.5, 68, 102 ]
[ 102, 68, 55.5, 0, -4 ]
[ 72, 88, 95, undefined ]

Notice the last line: undefined is never passed to your compare function. The specification moves every undefined to the end of the array before sorting starts, so it always finishes last, whatever you write. Empty slots in a sparse array behave the same way.

The compare function has to return a number

Writing a > b is the most common sorting bug in JavaScript, because a boolean is silently treated as 0 or 1 and the result is only half sorted. In TypeScript it never compiles:

const prices: number[] = [100, 25, 9];

prices.sort((a, b) => a > b);        // a comparator must return a number

tsc output:

sort_boolean.ts(3,13): error TS2345: Argument of type '(a: number, b: number) => boolean' is not assignable to parameter of type '(a: number, b: number) => number'.
  Type 'boolean' is not assignable to type 'number'.
Command Prompt showing TypeScript error TS2345 because a sort compare function returns a boolean instead of a number
Error TS2345: a comparator returning boolean is rejected at compile time.

Sort a string array alphabetically

The default sort compares UTF-16 code units, so every capital letter sorts before every lowercase one, and accented letters land after z. localeCompare() sorts the way a person would, and its options handle case and embedded numbers. There is a longer treatment in our guide to sorting an array alphabetically in TypeScript:

const names: string[] = ["Emma", "olivia", "Ashley", "bob"];

console.log([...names].sort());                                // UTF-16 code units
console.log([...names].sort((a, b) => a.localeCompare(b)));    // dictionary order

// how the two compare "a" and "A": -1 means "a" first, 0 means they are equal
console.log("default:", "a".localeCompare("A"),
            "| base:", "a".localeCompare("A", undefined, { sensitivity: "base" }));

const files: string[] = ["item10", "item9", "Item2"];
console.log([...files].sort());                                // "item10" before "item9"
console.log([...files].sort((a, b) => a.localeCompare(b, undefined, { numeric: true })));

Output:

[ 'Ashley', 'Emma', 'bob', 'olivia' ]
[ 'Ashley', 'bob', 'Emma', 'olivia' ]
default: -1 | base: 0
[ 'Item2', 'item10', 'item9' ]
[ 'Item2', 'item9', 'item10' ]
Command Prompt output comparing the default TypeScript string sort with localeCompare and numeric sorting of item9 and item10
"item10" before "item9" is the default; numeric: true fixes it.

The third line shows what the sensitivity option does: by default "a" sorts before "A", while sensitivity: "base" calls them equal, which is what you want when case should never decide the order. For large arrays, build one Intl.Collator and pass its compare method instead of calling localeCompare() on every comparison; there are numbers for that further down.

sort() changes your array: use toSorted() or a copy

sort() returns the very same array it sorted, so anything else holding that array sees the new order. In React state, or anywhere a value is shared, sort a copy. toSorted() is the ES2023 way; it needs "lib": ["es2023", "dom"] and Node.js 20 or later:

const original: string[] = ["Seattle", "Austin", "Denver"];

const sorted = original.sort();                 // same array, not a copy
console.log(sorted === original, original);

const untouched: string[] = ["Seattle", "Austin", "Denver"];
console.log([...untouched].sort(), untouched);          // copy first
console.log(untouched.toSorted(), untouched);           // ES2023: new array

Output:

true [ 'Austin', 'Denver', 'Seattle' ]
[ 'Austin', 'Denver', 'Seattle' ] [ 'Seattle', 'Austin', 'Denver' ]
[ 'Austin', 'Denver', 'Seattle' ] [ 'Seattle', 'Austin', 'Denver' ]

Because sorting mutates, TypeScript does not let you call sort() on a readonly array at all:

const regions: readonly string[] = ["West", "East", "Central"];

regions.sort();          // readonly arrays have no sort()

tsc output:

sort_readonly.ts(3,9): error TS2339: Property 'sort' does not exist on type 'readonly string[]'.

Sort an array of objects

Objects follow the same rules, applied to one property: subtract for numbers, localeCompare() for text, and getTime() for dates, since subtracting two Date values is a type error. For the full treatment, including a type-safe generic sortBy helper, see sorting an array of objects by property, and sorting arrays by date for date-heavy data:

interface Order {
  id: string;
  customer: string;
  total: number;
  placed: Date;
}

const orders: Order[] = [
  { id: "ORD-1001", customer: "Emma", total: 249.99, placed: new Date("2025-03-14") },
  { id: "ORD-1002", customer: "michael", total: 89.5, placed: new Date("2024-11-02") },
  { id: "ORD-1003", customer: "Olivia", total: 430, placed: new Date("2025-01-20") },
];

console.log([...orders].sort((a, b) => b.total - a.total).map(o => `${o.id} $${o.total}`));
console.log([...orders].sort((a, b) => a.customer.localeCompare(b.customer)).map(o => o.customer));
console.log([...orders].sort((a, b) => a.placed.getTime() - b.placed.getTime())
  .map(o => o.placed.toISOString().slice(0, 10)));

Output:

[ 'ORD-1003 $430', 'ORD-1001 $249.99', 'ORD-1002 $89.5' ]
[ 'Emma', 'michael', 'Olivia' ]
[ '2024-11-02', '2025-01-20', '2025-03-14' ]

Sorting is stable, so you can sort twice

Since ES2019, elements that compare as equal keep their original order. That means sorting by one key and then by another gives you a proper two-level sort, which is handy when a user clicks column headings one after another. You can also do both in one comparator with ||, as most object-array work ends up doing:

interface Employee {
  team: string;
  name: string;
}

const staff: Employee[] = [
  { team: "Sales", name: "Ann" },
  { team: "Ops", name: "Zoe" },
  { team: "Sales", name: "Bob" },
  { team: "Ops", name: "Amy" },
];

const byName = [...staff].sort((a, b) => a.name.localeCompare(b.name));
console.log(byName.map(e => `${e.team}/${e.name}`).join(" "));

const byTeamThenName = [...byName].sort((a, b) => a.team.localeCompare(b.team));
console.log(byTeamThenName.map(e => `${e.team}/${e.name}`).join(" "));

// or do it in one comparator
const oneGo = [...staff].sort((a, b) => a.team.localeCompare(b.team) || a.name.localeCompare(b.name));
console.log(oneGo.map(e => `${e.team}/${e.name}`).join(" "));

Output:

Ops/Amy Sales/Ann Sales/Bob Ops/Zoe
Ops/Amy Ops/Zoe Sales/Ann Sales/Bob
Ops/Amy Ops/Zoe Sales/Ann Sales/Bob
Command Prompt output showing a stable TypeScript sort where sorting by name and then by team keeps names in order inside each team
Sorted by name, then by team: inside each team the names are still in order.

Sort by a custom order, not by value

Sometimes the right order is not alphabetical or numeric at all. Map each value to a rank and subtract the ranks, which also keeps the compiler on your side: a Record<Priority, number> will not compile until every priority has a rank:

type Priority = "high" | "medium" | "low";

interface Ticket {
  id: number;
  priority: Priority;
}

const tickets: Ticket[] = [
  { id: 1, priority: "low" },
  { id: 2, priority: "high" },
  { id: 3, priority: "medium" },
  { id: 4, priority: "high" },
];

const order: Record<Priority, number> = { high: 0, medium: 1, low: 2 };

const queue = [...tickets].sort((a, b) => order[a.priority] - order[b.priority]);
console.log(queue.map(t => `#${t.id} ${t.priority}`).join(", "));

Output:

#2 high, #4 high, #3 medium, #1 low

Null values and numbers that arrive as text

API data is rarely tidy. Decide where null belongs and handle it before comparing, and convert numeric strings with Number(), or "1200.50" will sort before "89.00". The same applies when you sort by a date that arrives as a string:

interface Invoice {
  id: string;
  paid: string | null;         // an ISO date, or null when unpaid
  amount: string;              // the API sends numbers as text
}

const invoices: Invoice[] = [
  { id: "INV-3", paid: null, amount: "1200.50" },
  { id: "INV-1", paid: "2025-02-10", amount: "89.00" },
  { id: "INV-2", paid: "2024-12-01", amount: "430.75" },
];

// nulls last, then oldest payment first
const byPaid = [...invoices].sort((a, b) =>
  a.paid === null ? 1 : b.paid === null ? -1 : a.paid.localeCompare(b.paid)
);
console.log(byPaid.map(i => `${i.id} ${i.paid ?? "unpaid"}`).join(" | "));

// convert text to numbers before comparing, or "1200.50" sorts before "89.00"
console.log([...invoices].sort((a, b) => a.amount.localeCompare(b.amount)).map(i => i.amount));
console.log([...invoices].sort((a, b) => Number(a.amount) - Number(b.amount)).map(i => i.amount));

Output:

INV-2 2024-12-01 | INV-1 2025-02-10 | INV-3 unpaid
[ '1200.50', '430.75', '89.00' ]
[ '89.00', '430.75', '1200.50' ]

How fast is sorting, and does localeCompare cost much?

Sorting 100,000 email addresses three ways:

const emails: string[] = Array.from({ length: 100_000 }, (_, i) => `user${(i * 7919) % 100_000}@example.com`);

function time(label: string, compare?: (a: string, b: string) => number): void {
  const copy = [...emails];
  const start = performance.now();
  copy.sort(compare);
  console.log(label.padEnd(24), (performance.now() - start).toFixed(1).padStart(7), "ms", "first", copy[0]);
}

const collator = new Intl.Collator("en");

time("default sort()");
time("localeCompare", (a, b) => a.localeCompare(b));
time("Intl.Collator", collator.compare);
Command Prompt output timing the default TypeScript sort against localeCompare and Intl.Collator on one hundred thousand strings
The default sort is quickest, and a reused Intl.Collator beats localeCompare().

The plain sort() only compares code units, which is why it wins on speed and loses on correctness for anything a human will read. Between the two correct options, one reused Intl.Collator is meaningfully faster than calling localeCompare() on every comparison, because the collator is built once instead of a hundred thousand times.

Other TypeScript array guides worth reading next:

Frequently asked questions

How do I sort an array in TypeScript?

Pass a compare function to sort(): arr.sort((a, b) => a - b) for numbers, arr.sort((a, b) => a.localeCompare(b)) for strings.

Why does sort() put 100 before 25?

Without a compare function, sort() turns every element into a string and compares character by character, so "100" sorts before "25".

How do I sort in descending order?

Reverse the subtraction: (a, b) => b - a for numbers, or (a, b) => b.localeCompare(a) for text.

Does sort() change the original array in TypeScript?

Yes, it sorts in place and returns the same array. Use arr.toSorted(fn) or [...arr].sort(fn) to keep the original.

How do I sort strings ignoring upper and lower case?

Use a.localeCompare(b, undefined, { sensitivity: "base" }), which treats “a” and “A” as the same letter.

Why can’t I call sort() on a readonly array?

Sorting mutates the array, so readonly arrays do not have the method. Copy it first, or use toSorted().

Is TypeScript’s sort stable?

Yes. Since ES2019 equal elements keep their original order, so sorting by one key and then another produces a reliable two-level sort.