TypeScript typed comparators and multi-key sorting
Build comparators from a key with keyof, and combine them to sort by several fields.
On this page
TypeScript generics and keyof.
- Build comparators from a key with keyof, and combine them to sort by several fields.
- Predict the sample output and explain the state changes.
- Complete the practice task and check the boundary cases.
The concept
Array.prototype.sort takes a comparator, a function that returns a negative number, zero, or a positive number. Writing the same comparison logic repeatedly is error-prone. A helper like by(key) builds a comparator for any property of T, and the constraint K extends keyof T makes the compiler reject a property that does not exist.
To sort by several keys, try the first comparator and fall back to the next when it returns zero. The sort is stable in modern engines, so records with equal keys keep their previous order. Sorting mutates the array, so sort a copy when the original order matters.
Code example
interface Employee {
name: string;
dept: string;
salary: number;
}
type Comparator<T> = (a: T, b: T) => number;
function by<T, K extends keyof T>(key: K, direction: "asc" | "desc" = "asc"): Comparator<T> {
const sign = direction === "asc" ? 1 : -1;
return (a, b) => (a[key] < b[key] ? -sign : a[key] > b[key] ? sign : 0);
}
function thenBy<T>(...comparators: Comparator<T>[]): Comparator<T> {
return (a, b) => {
for (const compare of comparators) {
const result = compare(a, b);
if (result !== 0) {
return result;
}
}
return 0;
};
}
const staff: Employee[] = [
{ name: "Ada", dept: "Eng", salary: 120 },
{ name: "Bo", dept: "Ops", salary: 90 },
{ name: "Cy", dept: "Eng", salary: 100 },
{ name: "Di", dept: "Ops", salary: 90 },
];
const sorted = [...staff].sort(thenBy<Employee>(by("dept"), by("salary", "desc"), by("name")));
console.log(sorted.map((e) => e.dept + ":" + e.name + ":" + e.salary).join(" "));
console.log(staff.map((e) => e.name).join(" "));Eng:Ada:120 Eng:Cy:100 Ops:Bo:90 Ops:Di:90 Ada Bo Cy Di
Follow the code
Open the walkthrough to inspect the example one step at a time.
Show guided walkthrough
interface Employee { name: string; dept: string; salary: number;} type Comparator<T> = (a: T, b: T) => number; function by<T, K extends keyof T>(key: K, direction: "asc" | "desc" = "asc"): Comparator<T> { const sign = direction === "asc" ? 1 : -1; return (a, b) => (a[key] < b[key] ? -sign : a[key] > b[key] ? sign : 0);} function thenBy<T>(...comparators: Comparator<T>[]): Comparator<T> { return (a, b) => { for (const compare of comparators) { const result = compare(a, b); if (result !== 0) { return result; } } return 0; };} const staff: Employee[] = [ { name: "Ada", dept: "Eng", salary: 120 }, { name: "Bo", dept: "Ops", salary: 90 }, { name: "Cy", dept: "Eng", salary: 100 }, { name: "Di", dept: "Ops", salary: 90 },]; const sorted = [...staff].sort(thenBy<Employee>(by("dept"), by("salary", "desc"), by("name")));console.log(sorted.map((e) => e.dept + ":" + e.name + ":" + e.salary).join(" "));console.log(staff.map((e) => e.name).join(" "));Follow the example
Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.
View the complete dry-run table
| Step / line | What happens | State after step | Output so far |
|---|---|---|---|
| K must be a real property name of T, so a typo is a compile error. | — | — | |
| Return a negative, positive, or zero value according to the direction. | — | — | |
| Try each comparator in turn; the first non-zero answer decides. | — | — | |
| Sort by department, then by salary descending, then by name. The spread copies the array first. | — | — | |
| Eng before Ops; within Eng, 120 before 100; within Ops, equal salaries are ordered by name. | — | Eng:Ada:120 Eng:Cy:100 Ops:Bo:90 Ops:Di:90 | |
| The original array is unchanged because the sort worked on a copy. | — | Eng:Ada:120 Eng:Cy:100 Ops:Bo:90 Ops:Di:90 Ada Bo Cy Di |
Behind the scenes
How TypeScript processes this code
TypeScript checks types and emits JavaScript. Type annotations are removed before execution.
Cost and common mistakes
Sorting is O(n log n) comparisons; each comparison runs at most k small checks for k keys.
- Comparing with < works for numbers and strings, but not for every property type. Restrict the property type if needed.
- Subtracting two numbers is a shortcut that fails for Infinity and NaN. The explicit comparisons are safer.
- Mixing up asc and desc is easy. A named direction is clearer than a negative sign.
Try it yourself
How would you sort by name descending?
Reveal answer and reasoning
Use by("name", "desc") as one of the comparators, for example staff.sort(by<Employee, "name">("name", "desc")).