TypeScript Sorting and Custom Comparators: The Interview Cheat Sheet

- Default sort is lexicographic: without a comparator,
Array.sort()coerces elements to strings, so[10, 2, 1]sorts as[1, 10, 2] - Comparator return value: negative =
afirst, positive =bfirst, zero = equal. Anchor toa - bsubtraction so you never misremember under pressure - Stable since ES2019: equal-key elements preserve relative order on Node 18+; you can rely on this in LeetCode
- Sort mutates in place: copy with
[...arr].sort(cmp)or usearr.toSorted(cmp)(Node 20+) to preserve the original array undefinedskips your comparator: spec movesundefinedelements to the end regardless of your comparison function- Multi-key shorthand:
(a, b) => cmp1(a, b) || cmp2(a, b)exploits falsy zero to fall back to a secondary sort key automatically - Subtraction traps: never subtract strings (returns
NaN), and guard sentinel values againstInfinity - Infinity
You know Array.sort() takes a comparator. You've written (a, b) => a - b enough times it lives in muscle memory. What you probably cannot do under interview pressure is explain what the return value actually means, name the four ways the default silently destroys you, or write a stable multi-key sort without second-guessing yourself. Somewhere out there, a bug report is quietly loading because someone skipped the comparator. This is the reference.
The Default Sort Will Betray You
Here is the trap that burns more candidates than almost any other sorting question:
const nums = [10, 2, 1, 20, 3]; console.log(nums.sort()); // [1, 10, 2, 20, 3]
Take a second with that output. It is not a glitch. It is working exactly as intended. Without a comparator, Array.prototype.sort coerces every element to a string and compares by UTF-16 code units. So 10 becomes "10", and "10" < "2" because the character "1" has a lower code point than "2". Numbers sort lexicographically, not numerically. The bug is completely silent. Your test case with [1, 2, 3] passes. The one with [1, 2, 10] does not. Prod will let you know.
This is not a TypeScript quirk. It is intentional per the ECMAScript specification. TypeScript adds types on top of the same runtime, so it will not save you. The typescript-eslint rule require-array-sort-compare exists precisely because this bug is common enough that someone wrote a linter to yell at developers about it automatically.
Always pass a comparator. Always.
How TypeScript Sort Comparators Work
The comparator (a, b) => number encodes a comparison with three outcomes:
| Return value | Meaning |
|---|---|
| Negative | a comes before b |
| Positive | b comes before a |
| Zero | Treat as equal, preserve relative order |
Anchor it to subtraction: the return value is a - b on an imaginary number line. Negative means a is smaller, so it goes left. A lot of engineers memorize "negative means a first" without the reasoning, then blank on it mid-interview when the pressure hits. The subtraction model sticks.
A comparator must be consistent: if cmp(a, b) < 0, then cmp(b, a) must be > 0. Violate transitivity and the engine is allowed to produce any order. In practice, the result depends on the sort algorithm's internal access pattern, which means it looks random and changes between browser versions. Great way to get a Heisenbug.
Since ECMAScript 2019, the sort must be stable. V8 switched to TimSort in Chrome 70 and Node.js 11, so equal-key elements preserve their original relative order. On LeetCode (Node 18+), you can rely on stability. For the full story on what stability means and when it matters, see Stable Sort: The Property That Makes Multi-Key Sorting Actually Work.
The Five Patterns That Cover Every Interview Problem
Numbers, ascending and descending
const asc = nums.sort((a, b) => a - b); const desc = nums.sort((a, b) => b - a);
The subtraction trick works because JavaScript numbers are 64-bit floats with no integer overflow in the traditional sense. One edge case: NaN. If either element is NaN, the subtraction returns NaN, which the engine treats as 0 and quietly scrambles the result. If your input can contain NaN, use explicit comparison.
Strings
const words = ["banana", "Apple", "cherry"]; words.sort((a, b) => a.localeCompare(b)); // case-insensitive, locale-aware words.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); // byte order, fast
localeCompare handles Unicode and case correctly. The explicit ternary is faster for ASCII-only data. Never use a - b on strings. Subtracting two strings returns NaN, which you now know is bad.
Objects by a single property
interface Task { name: string; priority: number; } tasks.sort((a, b) => a.priority - b.priority); tasks.sort((a, b) => a.name.localeCompare(b.name));
Multi-key sort
Sort by the primary key, break ties with a secondary:
tasks.sort((a, b) => { const byPriority = a.priority - b.priority; if (byPriority !== 0) return byPriority; return a.name.localeCompare(b.name); });
This is the pattern for interval sorting, coordinate sorting, and any "sort by X, break ties by Y" problem. Those come up constantly.
Frequency sort
const freq = new Map<number, number>(); for (const n of nums) freq.set(n, (freq.get(n) ?? 0) + 1); nums.sort((a, b) => (freq.get(b) ?? 0) - (freq.get(a) ?? 0));
Build the frequency map first, then sort by it. The ?? 0 guard handles TypeScript's strict null checks for elements that theoretically might not be in the map. In an interview you will be tempted to skip it. TypeScript will make you pay.
The Traps That Actually Fail Candidates
Trap 1: Subtraction with non-finite values
const vals = [Infinity, -Infinity, 5, 3]; vals.sort((a, b) => a - b); // fine here // But Infinity - Infinity = NaN. Watch for sentinel values like Number.MAX_SAFE_INTEGER.
If a problem uses sentinels, use explicit comparison. The subtraction shorthand is 90% reliable and silently wrong the other 10%. That 10% has a habit of hiding in the one test case your interviewer is watching closely.
Trap 2: Sort mutates the original array
This one has a special cruelty because the bug is invisible until it matters.
const original = [3, 1, 2]; const sorted = original.sort((a, b) => a - b); console.log(original); // [1, 2, 3], original is gone console.log(sorted === original); // true, same reference
Array.sort sorts in place and returns the same array. The return value is not a copy. It is a reference to the array you just destroyed. If you need to keep the original, copy first:
const sorted = [...original].sort((a, b) => a - b); // or const sorted = original.slice().sort((a, b) => a - b);
ES2023 added Array.prototype.toSorted(), which returns a new sorted array without touching the original. Available in Node.js 20+ and modern browsers, but not guaranteed on LeetCode. Safe bet: use the spread copy.
Trap 3: Using a random comparator as a shuffle
// This is NOT a correct shuffle arr.sort(() => Math.random() - 0.5);
The comparator must be consistent between calls. A random comparator violates transitivity, and the resulting distribution is biased by the sort algorithm's access pattern. You will get some permutations far more often than others, and there is no easy way to tell which ones. For a correct shuffle, use Fisher-Yates. The difference matters more than you would expect.
Trap 4: undefined ignores your comparator entirely
Per spec, undefined elements are always moved to the end of the array, and the comparator is never called with them. null is not special: it gets coerced to the string "null" by the default sort and passed to your comparator like any other value. Know the difference. It has appeared in interviews, and explaining it well signals you understand the runtime, not just the syntax.
Where TypeScript Actually Helps
TypeScript catches the obvious type error at compile time:
// TypeScript error: operator '-' cannot be applied to type 'string' words.sort((a, b) => a - b);
You can also write a reusable typed comparator factory:
type Comparator<T> = (a: T, b: T) => number; function byKey<T, K extends keyof T>(key: K): Comparator<T> { return (a, b) => { const av = a[key]; const bv = b[key]; if (typeof av === "number" && typeof bv === "number") return av - bv; return String(av).localeCompare(String(bv)); }; } tasks.sort(byKey("priority"));
In a timed interview, you probably will not write a factory. But knowing the type signature (a: T, b: T) => number is enough to write clean, readable comparators inline without hesitation. For a broader cheat sheet covering TypeScript's collections and runtime traps, see TypeScript for Coding Interviews.
Quick Reference
| Goal | Comparator |
|---|---|
| Numbers ascending | (a, b) => a - b |
| Numbers descending | (a, b) => b - a |
| Strings ascending | (a, b) => a.localeCompare(b) |
| Strings descending | (a, b) => b.localeCompare(a) |
| Objects by numeric field | (a, b) => a.field - b.field |
| Objects by string field | (a, b) => a.field.localeCompare(b.field) |
| Multi-key (primary, fallback) | `(a, b) => (cmp1(a, b) |
| Non-mutating sort | [...arr].sort(cmp) or arr.toSorted(cmp) (Node 20+) |
The || shorthand for multi-key works because 0 is falsy. When the first comparator returns 0 (a tie), the second runs automatically. Two lines instead of five. Take it.
Narrate the Sort
Interviewers probe sorting two ways. First: "sort this array of intervals by start time," which is direct and obvious. Second: sorting is preprocessing before the real algorithm, and you have to justify why.
When sorting is preprocessing, say so out loud. "I'm going to sort by start time because it lets me apply a greedy scan." The interviewer wants to know you understand the O(n log n) cost and why it unlocks your approach. Sorting is not a free black box in an interview. The JavaScript for Coding Interviews guide covers the broader set of missing data structures and gotchas that apply equally in TypeScript.
Narrating while you code is its own skill, separate from knowing comparators cold. SpaceComplexity runs voice-based mock interviews with rubric feedback across communication, problem-solving, and code quality, so you can rehearse the explanation, not just the code.
Further Reading
- Array.prototype.sort() specification and behavior (MDN Web Docs)
- Getting things sorted in V8: the move to TimSort (V8 Engineering Blog)
- Stable Array.prototype.sort in V8 (V8 Features)
- ECMAScript 2026 Language Specification: Array.prototype.sort (TC39)
- require-array-sort-compare ESLint rule (typescript-eslint)