How do you write a single comparator for Array.prototype.sort that orders employee records by department name ascending and then by salary descending?
answer
- compare keys in priority order
- return the first non-zero result
- a tie is 0, and 0 is falsy
- chain the terms with ||
- swap operands to flip one key
basics
~20 sChain the per-key comparisons with the logical OR operator: (a, b) => a.dept.localeCompare(b.dept) || b.salary - a.salary. A zero from the first key is falsy, so evaluation falls through to the tiebreaker; reversing a key means swapping its two operands.
solid answer
~50 sWrite one comparator that evaluates the keys in priority order and returns the first non-zero result. The `||` operator does that for you, because a tie returns `0`, which is falsy: `(a, b) => a.dept.localeCompare(b.dept) || b.salary - a.salary`. The first term orders departments ascending; if it yields `0` the second term runs and orders salary descending, since subtracting `a` from `b` puts the larger value first. Direction is controlled purely by which operand goes first, so flipping one key never affects the other. For three or more keys you just add another `||` term. The alternative — sorting once per key from least significant to most significant — also works because `Array.prototype.sort` is required to be stable since ES2019, but a single comparator is one pass instead of N and keeps the whole ordering rule in one place.
code
javascript · 12 linesconst staff = [
{ dept: 'Sales', salary: 90, name: 'Ada' },
{ dept: 'Eng', salary: 120, name: 'Bo' },
{ dept: 'Sales', salary: 140, name: 'Cy' },
{ dept: 'Eng', salary: 120, name: 'Di' },
];
const cmp = (a, b) =>
a.dept.localeCompare(b.dept) || b.salary - a.salary || a.name.localeCompare(b.name);
console.log([...staff].sort(cmp).map(r => `${r.dept} ${r.salary} ${r.name}`));
// [ 'Eng 120 Bo', 'Eng 120 Di', 'Sales 140 Cy', 'Sales 90 Ada' ]go deeper
Be able to write the chained form from memory and read it correctly: first key wins unless it returns zero, then the next key decides. Know that swapping the two operands reverses that one key.
Explain why the chain works — a tie is 0, 0 is falsy, so || short-circuits — and give the multi-pass alternative that leans on sort stability. Note that NaN is also falsy and will fall through unnoticed.
Bring up determinism and cost: add a final unique-key tiebreak so equal records do not shuffle between runs, and move expensive key derivation out of the comparator when the dataset is large.
Decide where ordering belongs — a chained comparator in the client, an ORDER BY in the query, or a precomputed sort key on the record — and make the choice consistent so paginated and in-memory views cannot disagree about order.
## The problem A multi-key sort orders by a primary key and uses a secondary key only to break ties. "Department ascending, then salary descending" means: group the records by department in alphabetical order, and within each department put the highest earner first. ## The single-comparator solution A comparator returns a signed number. So a multi-key comparator is simply: evaluate the keys in priority order and return the first non-zero result. ```js const byDeptThenSalary = (a, b) => a.dept.localeCompare(b.dept) || b.salary - a.salary; ``` The `||` operator gives you exactly this control flow for free. `||` evaluates its left operand and, if that value is falsy, evaluates and returns the right one. A comparator that reports a tie returns `0`, which is falsy, so the salary comparison runs only when the departments match. If the departments differ, the non-zero result short-circuits and the salary is never even looked at. Writing it out longhand makes the equivalence obvious: ```js const byDeptThenSalary = (a, b) => { const byDept = a.dept.localeCompare(b.dept); if (byDept !== 0) return byDept; return b.salary - a.salary; }; ``` ## Controlling direction per key Direction is decided entirely by operand order within a term: - ascending: `a.key - b.key`, or `a.key.localeCompare(b.key)` - descending: `b.key - a.key`, or `b.key.localeCompare(a.key)` Because each term is independent, one key can ascend while another descends. Negating a whole chained expression (`-(x || y)`) does **not** reverse a multi-key sort correctly in general, because it flips only the term that happened to be non-zero; it happens to work here but obscures intent. Prefer flipping operands per key. ## The falsy-value caveat `||` skips its left operand when the value is falsy — for numbers that means `0`, `-0`, and `NaN`. For `0` and `-0` this is exactly what you want: both mean "tie", so falling through to the tiebreaker is correct. `NaN` is the case to worry about: a broken term producing `NaN` will silently hand control to the next key rather than announcing a problem. That is one more reason to guarantee every term returns a real number. ## The multi-pass alternative Since ES2019, `Array.prototype.sort` is required to be stable, meaning elements the comparator reports as equal keep their original relative order. That makes a different technique valid: sort by the **least** significant key first, then by the more significant one. ```js const out = [...rows] .sort((a, b) => b.salary - a.salary) // least significant first .sort((a, b) => a.dept.localeCompare(b.dept)); // most significant last ``` The second pass groups by department, and stability preserves the salary order established by the first pass inside each group. This is genuinely useful for interactive tables where the user clicks column headers one at a time and expects the previous ordering to survive as a tiebreak. For a fixed ordering rule known up front, the single chained comparator is better: one pass instead of N, and the rule is readable in one place. ## Building comparators generically When sort keys are configuration rather than code, build the chain from a list: ```js const chain = (...cmps) => (a, b) => { for (const cmp of cmps) { const r = cmp(a, b); if (r !== 0) return r; } return 0; }; const cmp = chain( (a, b) => a.dept.localeCompare(b.dept), (a, b) => b.salary - a.salary, (a, b) => a.id - b.id ); ``` A final total-order tiebreak such as `id` is worth adding when the output must be identical on every run: without it, records equal on every declared key rely on input order, which may itself vary between requests. ## Cost The comparator runs many times per sort, so keep each term cheap. `localeCompare` in particular does real collation work; if the array is large and the key is derived (parsing a date string, lowercasing, formatting), compute the key once per element into a temporary array, sort that, then map back — instead of recomputing it inside every comparison.
- Could you get the same ordering by calling sort twice instead?Yes, if you sort by the least significant key first and the most significant key last, and you rely on `Array.prototype.sort` being stable — guaranteed since ES2019 — so that ties keep the order the earlier pass established. It costs N passes instead of one, but it fits interactive tables where the user picks columns one at a time.
- Is there a risk in using || to chain comparator terms?One: `||` falls through on any falsy value, which for numbers means `0`, `-0`, and `NaN`. `0` and `-0` both mean "tie", so falling through is correct. `NaN` is not a tie — it is a broken term — and it will silently pass control to the next key. Make sure every term returns a real number.
- The comparator recomputes an expensive key on every call. What would you do?Compute each element's sort key once, sort by the precomputed keys, then drop them — the decorate-sort-undecorate pattern. Map each element to `{ key, item }`, sort on `key`, then map back to `item`. A sort makes on the order of n log n comparisons, so per-comparison work like parsing or collation is paid far more often than per-element work.
saying these in an interview costs you the question
- Sorting by the primary key first in a multi-pass approach
- Negating the whole chained expression to reverse one key
- Believing || adds or averages the comparator results
- Returning booleans from individual comparison terms
- Recomputing a parsed or formatted key inside every comparison