v0.4.0  ·  Zero dependencies  ·  MIT

The standard library
JavaScript forgot.

Koyo is a tiny, zero-dependency utility library for TypeScript and JavaScript — now with reactive state, a typed $, and one error type across the library. Runtime-agnostic: works identically on Node.js, Bun, browsers, and edge runtimes.

$npm install koyojs
Get started GitHub

Built right.

Koyo solves the inconsistencies that make JavaScript's standard tooling frustrating to depend on.

Zero dependencies

One file per function. No transitive bloat, no lockfile surprises. Just your code and Koyo.

Runtime-agnostic

Identical behaviour on Node.js, Bun, browsers (tested with Chromium via happy-dom), and edge runtimes. Write once, run everywhere.

TypeScript first

Full type definitions included. Ships as ESM, CJS, and a native Bun build out of the box.

Tree-shakeable

Import the whole library or per-module. Bundlers eliminate what you don't use.

Quick start

Import from the root or from per-module paths — both work.

String utilitiesSee all
import { capitalize, slugify, isEmail, toCamelCase } from "koyojs";

capitalize("hello world");
// → "Hello world"

slugify("Hello World!");
// → "hello-world"

isEmail("user@example.com");
// → true

toCamelCase("my-variable-name");
// → "myVariableName"
Number utilitiesSee all
import { clamp, randomInt, sum, average } from "koyojs";

clamp(150, 0, 100);
// → 100

randomInt(1, 10);
// → 7

sum([1, 2, 3, 4, 5]);
// → 15

average([10, 20, 30]);
// → 20
Array utilitiesSee all
import { chunk, unique, groupBy, flatten } from "koyojs";

chunk([1, 2, 3, 4, 5], 2);
// → [[1, 2], [3, 4], [5]]

unique(["a", "b", "a", "c"]);
// → ["a", "b", "c"]

groupBy([1, 2, 3, 4], n => n % 2 === 0 ? "even" : "odd");
// → { odd: [1, 3], even: [2, 4] }

flatten([1, [2, [3, 4]]]);
// → [1, 2, 3, 4]
Object utilitiesSee all
import { pick, omit, extract, cloneDeep } from "koyojs";

const user = { id: 1, name: "Alice", password: "s3cr3t" };

pick(user, ["id", "name"]);
// → { id: 1, name: "Alice" }

omit(user, ["password"]);
// → { id: 1, name: "Alice" }

extract({ db: { host: "localhost", port: 5432 } }, ["db.host"]);
// → { db: { host: "localhost" } }

cloneDeep(user);
// → fully independent deep copy
Math utilitiesSee all
import { gcd, factorial, isPrime, roundTo, isClose } from "koyojs";

gcd(48, 18);
// → 6

factorial(20);
// → 2432902008176640000n  (exact bigint)

isPrime(97);
// → true

roundTo(1.005, 2);
// → 1.01  (fixes the naive Math.round(1.005 * 100) / 100 bug)

isClose(0.1 + 0.2, 0.3);
// → true
State — reactivitySee all
import { signal, memo, effect, batch } from "koyojs";

const [count, setCount] = signal(0);
const doubled = memo(() => count() * 2);

effect(() => {
  console.log(count(), doubled());
});
// → 0 0, and again on every change — no dependency array

setCount(n => n + 1);
// → 1 2

batch(() => {
  setCount(5);
  setCount(6);
});
// → 6 12  (one run, not two)
DOM — typed $See all
import { $, ready } from "koyojs";

ready(() => {
  $(".card").addClass("loaded").css("opacity", 1);

  // Delegated — fires for rows added later too
  $("#table").on("click", ".row", function () {
    $(this).closest("tr").toggleClass("selected");
  });

  // A plain string is text, never markup
  $("#out").append("5 < 6");
  // → renders literally, injects nothing
});
Error — one error typeSee all
import { KoyoError, isKoyoError, assert } from "koyojs";

throw new KoyoError("Card declined", {
  code: "PAYMENT_DECLINED",
  module: "Billing",
  details: { last4: "4242" },
});

try {
  charge();
} catch (err) {
  if (isKoyoError(err)) {
    logger.error(err.toJSON());
    // → { name, code, module, message, details }
  }
}

assert(items.length > 0, "items must not be empty");
// → throws when falsy, and narrows the type after it

Fast by design.

Measured with mitata on Intel Core Ultra 7 155H · browser tab uses happy-dom (Chromium), where $ is compared against jQuery 4 · lower is faster.

String

capitalizens
toCamelCaseµs
toSnakeCaseµs
toTitleCaseµs
truncate / ellipsifyns
slugifyµs
isEmailns

Number

average (1 000 numbers)µs
sum (1 000 numbers)µs
randomInt (0–1000)ns
isEven / isOddns

Array

chunk (1 000 items, size 10)µs
flatten (500 mixed-depth items)µs
unique / removeDuplicates (500 items, 50 unique)µs
groupBy (200 items, 5 groups)µs
createArray / TypedArray push (10 items)ns

Object

pickns
extractµs
omitns
excludeµs
cloneDeepµs
cloneShallowµs

Math

gcd / lcmns
factorial(20)µs
isPrime(7919)ns
isqrt(100000000000000)µs
floorDiv / mod (-7, 2)ns
comb / perm (60, 30)µs
factorial(15): exact (bigint) vs unsafe (number)ns
isClose vs isCloseAbs / isCloseRelns
isClosens
sum (1 000 numbers, precision-lossy)µs
roundTo(1.005, 2)ns
degrees / radiansns
dist (3D points)ns

State

signal: readns
signal: write with no subscribersns
signal: write with 1 effect subscribedns
memo: cached read vs recomputens
batch: 10 writes, 1 effectµs
component: mount + render + disposeµs
component: re-render on setStatens
Dead-code elimination disclaimer — The benchmarks below (marked ⚠ DCE) were flagged by mitata as potentially optimized out by the runtime. Their numbers may represent best-case scenarios rather than real-world performance. See the mitata docs for guidance on writing benchmarks that resist dead-code elimination.
clampns⚠ DCE
koyojslodashremedaradashjQueryJS / regex baselinehappy-dom

Ready to use.

Pick your runtime. Koyo meets you there.

npmnpm install koyojs
bunbun add koyojs

Then import what you need:

// Named exports from root
import { slugify, clamp, chunk, pick, gcd, signal, $ } from "koyojs";

// Or per-module (better tree-shaking)
import { slugify }   from "koyojs/String";
import { clamp }     from "koyojs/Number";
import { chunk }     from "koyojs/Array";
import { pick }      from "koyojs/Object";
import { gcd }       from "koyojs/Math";
import { signal }    from "koyojs/State";
import { $ }         from "koyojs/DOM";
import { KoyoError } from "koyojs/Error";