npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ts-order

v0.0.5

Published

Type-safe Order utility for delarative, composable, and immutable multi-key ordering logic

Readme

🔢 ts-order

A tiny (968 B), type-safe sorting utility for JavaScript/TypeScript that gives you declarative, composable, and immutable multi-key ordering logic.

Features

  • Declarative key-based sorting
  • Composable chaining with .by(), .map(), .when()
  • Immutable API: methods return a new Order
  • Type-safe with full inference
  • DSU-optimized for costly key computations
  • Zero dependencies

Installation

# npm
npm install ts-order

# yarn
yarn add ts-order

# pnpm
pnpm add ts-order

# bun
bun add ts-order

Quickstart

import { Order } from 'ts-order';

interface User {
	id: number;
	isActive: boolean;
	firstName: string;
	lastName: string;
	age: number | null;
	createdAt: Date;
}

const users: User[] = [
	/* ... */
];

// Sort by isActive DESC, lastName ASC, then firstName ASC, then id ASC (tiebreaker)
const byActiveAndName = new Order<User>()
	.by((u) => u.isActive, { direction: 'desc' }) // active users first
	.by((u) => u.lastName)
	.by((u) => u.firstName)
	.by((u) => u.id); // tiebreaker stable sort on id

// Use order's .sort() method for DSU (decorate-sort-undecorate) optimized sorting (ensure's keys are computed only once per step)
const sorted = byActiveAndName.sort(users);

// Or use the comparator directly with native Array.prototype.sort
users.sort(byActiveAndName.compare);

API

class Order<T>

by(selector, options?)

class Order<T> {
	static by<K, T>(
		selector: (value: T) => K,
		options?: {
			direction?: 'asc' | 'desc';
			compare?: Comparator<K>;
			predicate?: (value: T) => boolean;
		},
	): Order<T>;
	by<K>(
		selector: (value: T) => K,
		options?: {
			direction?: 'asc' | 'desc';
			compare?: Comparator<K>;
			predicate?: (value: T) => boolean;
		},
	): Order<T>;
}

Create a new order with a single sort step. Defaults to an ascending direction and natural three-way comparison (i.e. a < b, a > b).

const byAgeAsc = Order.by((u: User) => u.age);

You can optionally provide a custom compare and direction property. Note: The compare property expects a comparator that sorts in an ascending direction; the direction property will flip the compare result when set to 'desc'.

import { nullsLast } from 'ts-order/comparator';

const byNameDesc = Order.by((u: User) => u.name, {
	direction: 'desc',
	compare: (a, b) => a.localeCompare(b),
});

const byExpiryAsc = Order.by((i: Item) => i.expiresAt, {
	compare: (a, b) => a.getTime() - b.getTime(),
});

const byAgeAscNullsLast = Order.by((u: User) => u.age, {
	compare: nullsLast((a, b) => a - b),
});

You may also optionally pass predicate to run a step only when both values satisfy the guard function.

const activeUsersFirst = Order.by((u: User) => u.isActive, {
	direction: 'desc',
	predicate: (u) => u.isActive,
});

Every Order instance also exposes a chainable .by() method to append additional sort steps.

const byCreatedThenId = new Order<User>()
	.by((u) => u.createdAt)
	.by((u) => u.id);

reverse(order) and reverse()

class Order<T> {
	static reverse<T>(order: Order<T>): Order<T>;
	reverse(): Order<T>;
}

Flip all step directions.

const newestFirst = Order.by((u: User) => u.createdAt).reverse();

map(outer, order)

class Order<T> {
	static map<T, K>(outer: (t: T) => K, sub: Order<K>): Order<T>;
	map<K>(outer: (t: T) => K, sub: Order<K>): Order<T>;
}

Lift an order defined for a nested value into the parent domain.

interface Address {
	city: string;
	postcode: string;
}
interface Customer {
	id: number;
	address: Address;
}

const byAddress = Order.by((a: Address) => a.city).by((a) => a.postcode);

const byCustomerAddress = Order.map((c: Customer) => c.address, byAddress);

// Or chain onto an existing order
const byIdThenAddress = new Order<Customer>()
	.by((c) => c.id)
	.map((c) => c.address, byAddress);

when(predicate, order)

class Order<T> {
	static when<T>(predicate: (value: T) => boolean, order: Order<T>): Order<T>;
	when(predicate: (value: T) => boolean, order: Order<T>): Order<T>;
}

Wrap an order with a guard so every step only runs when both values pass the predicate. This is handy for enabling blocks of steps conditionally or combining with per-step predicates.

const byRegion = new Order<User>()
	.by((u) => u.region)
	// EU region users get their own Order logic
	.when(
		(u) => u.region === 'eu',
		Order.by((u) => u.score, {
			direction: 'desc',
		}),
	)
	.by((u) => u.id); // tiebreak id sort for all users

compare

class Order<T> {
	get compare(): (a: T, b: T) => number;
}

Retrieve a native comparator compatible with Array.prototype.sort.

items.sort(Order.by((i: Item) => i.score, { direction: 'desc' }).compare);

sort(array, order) and sort(array)

class Order<T> {
	static sort<T>(array: readonly T[], order: Order<T>): T[];
	sort(array: readonly T[]): T[];
}

Sort an array and return a new array.

This method implements the Schwartzian Transform or DSU (decorate-sort-undecorate) technique, which ensures that each key selector is only invoked once per element per step. For larger arrays or costly key computations, this can yield significant performance improvements over repeatedly calling the selector during comparisons.

const out = Order.sort(users, byName);
// or
const out2 = byName.sort(users);

ts-order/comparator subpackage

The comparator subpackage offers a collection of standalone utilities you can use directly with native array sorting, or alongside Order when you need fine-grained control.

import {
	boolean,
	by,
	compare,
	date,
	localeString,
	nansFirst,
	nansLast,
	nullsFirst,
	nullsLast,
	number,
	order,
	reverse,
	string,
	when,
} from 'ts-order/comparator';

Comparators

compare(a, b)

function compare<T>(a: T, b: T): number;

Natural three-way comparator that relies on </> checks. Also exported as string.

['b', 'a', 'c'].sort(compare); // ['a', 'b', 'c']

nullsFirst and nullsLast

function nullsFirst<T>(compareFn: Comparator<T>): Comparator<T | null | undefined>;
function nullsLast<T>(compareFn: Comparator<T>): Comparator<T | null | undefined>;

Decorate a comparator to move null/undefined values to the beginning or end of the ordering.

scores.sort(nullsLast(number)); // [1, 2, null]

nansFirst and nansLast

function nansFirst<T>(compareFn: Comparator<T>): Comparator<T>;
function nansLast<T>(compareFn: Comparator<T>): Comparator<T>;

Handle NaN explicitly while delegating other values to the base comparator.

[Number.NaN, 2, 1].sort(nansLast(number)); // [1, 2, NaN]

number, boolean, date, localeString

function number(a: number, b: number): number;
function boolean(a: boolean, b: boolean): number;
function date(a: Date, b: Date): number;
function localeString(a: string, b: string): number;

Ready-to-use comparators for common primitives. localeString uses Intl.Collator under the hood.

scores.sort(number); // numeric sort asc (NaN's first)
users.sort(localeString); // locale-aware string sort asc
flags.sort(boolean); // boolean sort asc (false first)
createdAt.sort(date); // chronological date sort asc (invalid dates first)

Comparator builders & combinators

These functions let you build, adapt, and compose comparators for flexible multi-key or conditional sorting. Similar to the Order class methods, but working directly on comparator functions instead of Order instances.

by(key, options?)

function by<T, K>(
	key: (value: T) => K,
	options?: KeyOptions<K, T>,
): Comparator<T>;

Project values before comparing them. Accepts direction, compare, and predicate just like Order.by.

items.sort(by((item) => item.label));

order(...comparators)

function order<T>(...comparators: Comparator<T>[]): Comparator<T>;

Chain comparators from most to least significant.

users.sort(
	order(
		by((u) => u.lastName),
		by((u) => u.firstName),
	),
);

map(mapper, comparator?)

function map<T, U>(
	mapper: (value: T) => U,
	comparator?: Comparator<U>,
): Comparator<T>;

Adapt a comparator to operate on mapped values.

// Sort items by their nested score property
items.sort(map((item) => item.nested.score));

reverse(compareFn)

function reverse<T>(compareFn: Comparator<T>): Comparator<T>;

Flip the direction of a comparator.

events.sort(reverse(date)); // most recent first

when(predicate, comparator)

function when<T>(
	predicate: (value: T) => boolean,
	comparator: Comparator<T>,
): Comparator<T>;

Run a comparator only when both values pass a guard.

const evenNumbersFirst = order<number>(
	when((value) => value % 2 === 0, number),
	number,
);

License

MIT