TypeScript · Course navigation

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
Before you start

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

TypeScript
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(" "));
Output
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
Guided walkthroughReady
1interface Employee {
2 name: string;
3 dept: string;
4 salary: number;
5}
6
7type Comparator<T> = (a: T, b: T) => number;
8
9function by<T, K extends keyof T>(key: K, direction: "asc" | "desc" = "asc"): Comparator<T> {
10 const sign = direction === "asc" ? 1 : -1;
11 return (a, b) => (a[key] < b[key] ? -sign : a[key] > b[key] ? sign : 0);
12}
13
14function thenBy<T>(...comparators: Comparator<T>[]): Comparator<T> {
15 return (a, b) => {
16 for (const compare of comparators) {
17 const result = compare(a, b);
18 if (result !== 0) {
19 return result;
20 }
21 }
22 return 0;
23 };
24}
25
26const staff: Employee[] = [
27 { name: "Ada", dept: "Eng", salary: 120 },
28 { name: "Bo", dept: "Ops", salary: 90 },
29 { name: "Cy", dept: "Eng", salary: 100 },
30 { name: "Di", dept: "Ops", salary: 90 },
31];
32
33const sorted = [...staff].sort(thenBy<Employee>(by("dept"), by("salary", "desc"), by("name")));
34console.log(sorted.map((e) => e.dept + ":" + e.name + ":" + e.salary).join(" "));
35console.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
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput 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")).

Further reading