Library / SDK
Teun/thenBy.js avatar
Teun/thenBy.js

thenBy: multi-key sorting for JavaScript arrays

thenBy is a javascript micro library that helps sorting arrays on multiple keys

681 stars63 forksJavaScriptApache-2.0

At a glance

What is it?
thenBy is a micro library that lets you sort a JavaScript array on several keys with a firstBy().thenBy().thenBy() chain instead of hand-written nested comparators. The convenience costs runtime in hot sorts, and the package has had no release since version 1.4.1.
Who is it for?
thenBy earns a place in application code where sorts happen on request, on human-sized data, and where readable comparison chains beat nested comparator functions. It is the wrong choice for a hot loop over a large array, where the extra function calls per comparison outweigh the readability gain.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Activity is slowing. The repository last received commits 6 months ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Composing comparators instead of nesting them

Sorting a JavaScript array on more than one key normally means writing a comparator that compares the first key, falls back to the second, and falls back again. The result is a single function with nested conditionals that nobody enjoys reading. thenBy takes a different route. It exports a firstBy function that returns a comparator, and that comparator carries a thenBy method which returns a new comparator with the next key appended. The chain is passed straight to the native Array.sort method, which the README describes as the point of the library.

javascript
// first by length of name, then by population, then by ID
data.sort(
    firstBy(function (v1, v2) { return v1.name.length - v2.name.length; })
    .thenBy(function (v1, v2) { return v1.population - v2.population; })
    .thenBy(function (v1, v2) { return v1.id - v2.id; })
);

The comparator functions you supply are ordinary two-argument discriminators returning -1, 0 or 1, so nothing about the sorting semantics changes. What changes is the shape of the code. Each key is a separate expression, and adding a fourth key is one more link rather than another else-if branch inside one expression.

The library is described as a micro library and the repository layout backs that up. It ships thenBy.js, thenBy.esm.js, thenBy.module.js and a minified build, plus a TypeScript declaration file. There is no runtime dependency listed in package.json, so the whole thing is comparator construction logic and nothing else.

Property names and unary functions as shortcut keys

Most sorts do not need a custom comparator at all, they need a property name. thenBy accepts a string in place of a function and builds the comparison itself:

javascript
// first by length of name, then by population, then by ID
data.sort(
    firstBy(function (v1, v2) { return v1.name.length - v2.name.length; })
    .thenBy("population")
    .thenBy("id")
);

There is one documented edge case here. If an element does not have the property defined, it sorts as if it were the empty string, which the README says will typically place it at the top. For an array of optional fields that is a reasonable default, but it is a default rather than a rule you can configure, and a null population may end up ahead of a real one.

A unary function works too. Instead of a comparator that takes two values and subtracts them, you can pass a function that takes one item and returns its key, which shortens the first example to:

javascript
// first by length of name, then by population, then by ID
data.sort(
    firstBy(function (v) { return v.name.length; })
    .thenBy("population")
    .thenBy("id")
);

Standard JavaScript functions fit into that slot. Passing Number makes the sort numeric instead of lexical, which is the fix for the classic surprise where the array ["2", "20", "03", "-2", "0", 200, "2"] sorts as text:

javascript
var values = ["2", "20", "03", "-2", "0", 200, "2"];
var sorted = values.sort(firstBy(Number));

Direction and ignoreCase live in the options argument

The second argument of each key controls the sort direction. Passing -1 or the string desc reverses that key, so a single chain can mix ascending and descending keys:

javascript
// first by length of name descending, then by population descending, then by ID ascending
data.sort(
    firstBy(function (v1, v2) { return v1.name.length - v2.name.length; }, -1)
    .thenBy("population", "desc")
    .thenBy("id")
);

Since version 1.2.0 the shortcut methods accept an options object as well. If the second argument is a number it is read as the direction, and if it is an object the ignoreCase property turns on case-insensitive comparison:

javascript
// first by name, case insensitive, then by population
data.sort(
    firstBy("name", {ignoreCase:true})
    .thenBy("population")
);

Combining case-insensitive sorting with descending order requires the object form for direction too, because the two live in the same place:

javascript
// sort by name, case insensitive and descending
data.sort(firstBy("name", {ignoreCase:true, direction:"desc"}));

The API is therefore positional in one argument and named in the other, and mixing the two styles in a single chain is legal but easy to misread. A reviewer skimming a chain has to check whether a bare -1 is a direction or whether an object follows.

The options object also carries a cmp property for a fully custom compare function, which is the escape hatch for orderings that do not map to a property comparison. The README notes that cmp can be combined with direction but not with ignoreCase.

Intl.Collator handles the orders that string comparison gets wrong

The most useful custom comparator the README shows is the one JavaScript already provides. Intl.Collator knows the collation rules of a given culture, and its compare function can be handed to thenBy through the cmp option:

javascript
// in German, ä sorts with a
var germanCompare = new Intl.Collator('de').compare;
// in Swedish, ä sorts after z
var swedishCompare = new Intl.Collator('sv').compare;
data.sort(
    firstBy("name", {cmp: swedishCompare})
);

The comment in that example is the whole argument for collation-aware sorting. In German, ä belongs with a; in Swedish it belongs after z. Plain string comparison sorts by code point and gets both cases wrong for a user-facing list of names. The README also notes an environment caveat worth knowing before you rely on it: many browsers implement the collation data for many cultures, while in NodeJS the API is present but implemented for the English culture only. A server-rendered list sorted in Node will therefore not match a list sorted in the browser for non-English names.

The playing card example in the README shows the other use of cmp, where the ordering is a domain rule rather than a cultural one:

javascript
const cards = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A'];
var cardCompare = (c1, c2) =>{
    return cards.indexOf(c1) - cards.indexOf(c2);
}
var handOfCards =  [
        { id: 7, suit:"c", card:"A" },
        { id: 8, suit:"d", card:"10" },
        // etc
    ];
handOfCards.sort(firstBy("card", {cmp: cardCompare, direction: "desc"}));

Running the build and the test suite from the package scripts

The repository ships its own build script rather than relying on a bundler configuration inside package.json. Two scripts are defined, one for the test run and one for the build alone:

json
  "scripts": {
    "test": "node build.mjs && npx tsc --project tsconfig.json && node --test --test-reporter=tap tests/thenby.tests.js",
    "build": "node build.mjs"
  },

The test command chains three steps: build.mjs regenerates the distributable files, tsc type-checks the project against tsconfig.json, and the node test runner executes tests/thenby.tests.js with the TAP reporter. The build step uses terser, and typescript is also listed as a development dependency, both pinned to caret ranges in package.json. The presence of a tests directory and a type declaration file, thenBy.module.d.ts, means the package ships types to consumers rather than only to its own build.

The published entry points are declared explicitly in package.json, with main pointing at the CommonJS build, module at the ESM build, and types at the declaration file. The exports map routes import to the ESM file and require to the CommonJS file, with a default fallback to the CommonJS build for bundlers that consult neither condition. That is a modern packaging layout, and it means an older toolchain that ignores the exports map falls back to a file that still works.

Installation through npm or yarn is the path the README lists in its table of contents, and the package name on the registry is thenby.

The runtime cost shows up only on very large sorts

The README has a section on performance, and it is unusually blunt about the trade-off. thenBy builds a combined comparator by composing the functions you supply with small utility functions that reverse and combine the current order with the previous one. Constructing that combined function is cheap and, as the README puts it, should always be negligible. Running it is where the cost sits. The comparisons happen through several small functions calling each other rather than through one hand-written piece of code, and that indirection makes each comparison slower than a carefully written comparator.

The guidance is that this normally does not matter, but that on very large sets it can. The library also notes that if a specific field is always present and always a number, a hand-written comparator would be significantly faster, and it points at the unit tests, which contain an extreme example of that case.

This is the honest cost of the abstraction. A chain of three keys is three comparisons worth of work per pair of elements, each with extra call overhead, on top of the O(n log n) comparisons the sort itself performs. For a table of a few hundred rows sorted on user interaction, that difference is invisible. For a million-row sort inside a request handler, it is not, and at that point the readable chain is the wrong trade. The library also cannot express every ordering, so a complex domain rule tends to end up in a cmp function anyway, which puts back the very function you were trying to avoid.

A maintained-in-place library with no tagged releases

thenBy is a small library with a straightforward licence. The package metadata lists Apache-2.0, and a LICENSE.TXT file sits at the root of the repository. Apache-2.0 is a permissive licence with an explicit patent grant, which is a reasonable choice for a utility that ends up embedded in other people's build output, and the obligations it carries attach to redistribution of the library itself rather than to the data you sort with it.

The repository has no GitHub releases, so there is no tag to point a dependency at, and the version in package.json is 1.4.1. The last push was on 2026-04-20. The repository also carries an AGENTS.md file, a bower.json for the older distribution channel, a sample.htm for browser use, and a .editorconfig, which are signs of a project that has been around long enough to accumulate compatibility layers for older tooling.

For a package this small, the interesting question is not whether it is feature-complete but whether the shape of the API still fits your code. The fluent comparator chain is easy to read and easy to misuse, the empty-string fallback for missing properties is not configurable, and the ignoreCase option cannot be combined with a custom comparator. Those constraints are stable enough to plan around, and small enough that writing your own comparator for one specific sort is a reasonable alternative when the ordering rule is one-off.

Editorial conclusion

thenBy earns a place in application code where sorts happen on request, on human-sized data, and where readable comparison chains beat nested comparator functions. It is the wrong choice for a hot loop over a large array, where the extra function calls per comparison outweigh the readability gain. Before depending on it, check that the version you pull matches the module entry points your bundler expects, since the package publishes separate CommonJS, ESM and type declaration files, and note that the last push was on 2026-04-20, with no tagged release in the repository.

Frequently asked questions

How do I install thenBy in a JavaScript project?

The package is published on npm under the name thenby, and the README lists install using npm or yarn as well as installing in your HTML. The repository also ships prebuilt files, including thenBy.js, thenBy.esm.js and thenBy.module.js.

What is the difference between firstBy and thenBy in thenBy.js?

firstBy creates the first comparator in the chain and takes a discriminator function, a property name, or a unary function as the key. thenBy is the method on that comparator which appends the next key, and the whole chain is passed to the native Array.sort method.

How do I sort case-insensitively with thenBy?

Since v1.2.0 the shortcut methods accept an options object with an ignoreCase property, for example firstBy("name", {ignoreCase:true}). If you also want descending order you have to use the options syntax for direction as well, such as {ignoreCase:true, direction:"desc"}.

Does thenBy sort numbers correctly out of the box?

Not by default, since property names sort as text. The README shows passing the Number function as a unary key, values.sort(firstBy(Number)), so the sort compares numeric values instead of lexical ones.

How do I run the tests for thenBy.js?

The package.json test script runs node build.mjs, then npx tsc --project tsconfig.json, then node --test --test-reporter=tap tests/thenby.tests.js. The build script on its own is node build.mjs.

Official sources

  1. Issues
  2. License: Apache-2.0
  3. README
  4. Teun/thenBy.js on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/teun-thenby-js.svg)](https://hysenlabs.com/projects/teun-thenby-js)