DEV Community

Cover image for JavaScript has no sorted containers. I built one for TypeScript.
Johans Neira
Johans Neira

Posted on

JavaScript has no sorted containers. I built one for TypeScript.

JavaScript ships with Array, Set, and Map — but nothing that keeps its elements sorted as you insert. If you've ever built a leaderboard, an order book, or anything that answers "give me the items between X and Y", you know the workaround: push into an array and .sort() after every insertion. It works, until scale punishes you — you're paying O(n log n) over and over for data that was already 99.9% sorted.

Python solved this years ago with sortedcontainers, built on an elegant "list of lists" design instead of balanced trees. I just published sorted-collections, which brings that idea to TypeScript — with full credit to the original as its inspiration.

What you get

  • SortedList, SortedSet, SortedMap — always sorted, no manual re-sorting, range queries built in.
  • O(log n) insertions, O(√n) positional access via sqrt-decomposition into buckets.
  • Zero runtime dependencies, ~2KB gzip, types included, dual ESM/CJS.
  • Package quality gated in CI with publint, arethetypeswrong, and size-limit.

The API in 30 seconds

import { SortedList, SortedSet, SortedMap } from "sorted-collections";

// SortedList: stays sorted on every insert
const list = new SortedList([5, 1, 4, 2, 3]);
list.add(0);
console.log([...list]); // [0, 1, 2, 3, 4, 5]
console.log(list.at(2)); // 2 — positional access on sorted order

// SortedSet: no duplicates, plus set algebra
const a = new SortedSet([1, 2, 3, 4]);
const b = new SortedSet([3, 4, 5]);
console.log([...a.intersection(b)]); // [3, 4]

// SortedMap: keys always in order, range queries built in
const prices = new SortedMap<number, string>([
  [104.5, "order-3"],
  [99.2, "order-1"],
  [101.0, "order-2"],
]);
for (const [price, id] of prices.irange(100, 105)) {
  console.log(price, id); // 101.0 order-2, then 104.5 order-3
}
Enter fullscreen mode Exit fullscreen mode

Custom comparators are fully typed: number and string get natural ordering for free; for your own types, TypeScript requires a comparator at compile time — no silent string-coercion surprises.

Under the hood: buckets, not trees

Instead of a balanced tree of pointer-connected nodes, the data lives in many small contiguous arrays ("buckets"), each internally sorted, with an index of maximums on top. Locating the right bucket is a binary search; the operation itself touches only that small bucket. Contiguous memory is what modern CPUs are good at — that's the bet sortedcontainers made in Python, and it's the same one here.

One consequence of this design worth showing with real numbers: bulk construction. The constructors don't insert element by element — they sort once and slice directly into buckets. Here's bulk vs. per-element construction, in ops/sec (higher is better):

Structure n=1,000 n=100,000 n=1,000,000
SortedList 17,316/s vs 32,250/s 85/s vs 93/s 7/s vs 5/s
SortedSet 16,065/s vs 20,200/s 79/s vs 55/s 7/s vs 3/s
SortedMap 14,121/s vs 12,476/s 56/s vs 33/s 3/s vs 1/s

An honest reading, because benchmarks that only show wins aren't benchmarks:

  • SortedMap benefits at every scale, up to 3x faster at one million entries.
  • SortedSet pulls ahead from ~100,000 elements.
  • SortedList only wins clearly at the million-element scale — at small and mid sizes, the per-element path is competitive or ahead, and the absolute differences are microseconds.

If you hydrate large datasets — loading a snapshot, rebuilding an index — this is where the design pays off out of the box.

When you should NOT use this

Small datasets, or data you sort once and never touch again: a plain array with .sort() is simpler and probably faster. sorted-collections pays off when you insert and query continuously against data that keeps growing. The docs say this explicitly, with numbers.

Installation

npm install sorted-collections
Enter fullscreen mode Exit fullscreen mode

Works in Node and the browser, ESM or CJS, TypeScript or plain JS.

Reproduce everything

Every number in this post comes from the benchmark script in the repo — one command, fixed seed. If your hardware tells a different story, that's a bug report I want.

This is the library's first public release. Issues and PRs welcome — especially benchmarks I haven't thought of.

Top comments (0)