Skip to content

sortBy

Edit

Stably sorts a copied array from selector values computed once per item.

sortBy.ts
export type SortByValue = string | number | bigint | boolean | Date | null | undefined;
export type SortBySelector<T> = (this: void, value: T) => SortByValue;
const rank = (value: SortByValue): number => {
if (value == null) return 6;
if (value instanceof Date) return Number.isNaN(value.getTime()) ? 5 : 4;
if (typeof value === "number") return Number.isNaN(value) ? 5 : 0;
if (typeof value === "bigint") return 1;
if (typeof value === "string") return 2;
return 3;
};
const compare = (left: SortByValue, right: SortByValue): number => {
const leftRank = rank(left);
const rightRank = rank(right);
if (leftRank !== rightRank) return leftRank - rightRank;
if (leftRank >= 5 || Object.is(left, right)) return 0;
if (left instanceof Date && right instanceof Date) {
const a = left.getTime();
const b = right.getTime();
return a < b ? -1 : a > b ? 1 : 0;
}
if (typeof left === "number" && typeof right === "number") {
return left < right ? -1 : left > right ? 1 : 0;
}
if (typeof left === "bigint" && typeof right === "bigint") {
return left < right ? -1 : left > right ? 1 : 0;
}
if (typeof left === "string" && typeof right === "string") {
return left < right ? -1 : left > right ? 1 : 0;
}
if (typeof left === "boolean" && typeof right === "boolean") return left ? 1 : -1;
return 0;
};
/** Stably sorts a copy by selector values computed once per input item. */
export const sortBy = <T>(
array: readonly T[],
...selectors: ReadonlyArray<SortBySelector<T>>
): T[] => {
for (const selector of selectors) {
if (typeof selector !== "function") {
throw new TypeError("selectors must contain only functions");
}
}
const entries: Array<{ value: T; index: number }> = [];
for (let index = 0; index < array.length; index += 1) {
if (Object.hasOwn(array, index)) {
entries.push({ value: array[index] as T, index });
}
}
if (selectors.length === 0) return entries.map(({ value }) => value);
return entries
.map(({ value, index }) => ({
value,
index,
selected: selectors.map((selector) => selector(value))
}))
.sort((left, right) => {
for (let index = 0; index < selectors.length; index += 1) {
const order = compare(left.selected[index], right.selected[index]);
if (order !== 0) return order;
}
return left.index - right.index;
})
.map(({ value }) => value);
};
Terminal
wget -O src/lib/sortBy.ts https://raw.githubusercontent.com/jrTilak/lazykit/HEAD/registry/functions/sortBy.ts
sortBy.example.ts
import { sortBy, type SortBySelector } from "./sortBy";
type User = { name: string; age: number };
const selectors = [
(user: User) => user.age,
(user: User) => user.name
] satisfies ReadonlyArray<SortBySelector<User>>;
const users: User[] = sortBy(
[{ name: "Lin", age: 30 }, { name: "Ada", age: 30 }],
...selectors
);
// [{ name: "Ada", age: 30 }, { name: "Lin", age: 30 }]
  • array (readonly T[]) — Values to sort without mutation. Empty slots are skipped.
  • ...selectors — Produce strings, numbers, bigints, booleans, dates, or nullish values.
  • Selectors are evaluated exactly once per input item, including secondary selectors.
  • Selectors are invoked without a receiver.
  • Mixed values use the fixed order number, bigint, string, boolean, then valid date. NaN and invalid dates follow, with nullish values last.
  • Equal values and values within the same special group retain source order.
  • With no selectors, returns a shallow copy.