TypeScript typed comparators and multi-key sorting

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.

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(" "));
Read the full lesson