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.
Koyo solves the inconsistencies that make JavaScript's standard tooling frustrating to depend on.
One file per function. No transitive bloat, no lockfile surprises. Just your code and Koyo.
Identical behaviour on Node.js, Bun, browsers (tested with Chromium via happy-dom), and edge runtimes. Write once, run everywhere.
Full type definitions included. Ships as ESM, CJS, and a native Bun build out of the box.
Import the whole library or per-module. Bundlers eliminate what you don't use.
Import from the root or from per-module paths — both work.
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"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]);
// → 20import { 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]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 copyimport { 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);
// → trueimport { 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)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
});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 itMeasured with mitata on Intel Core Ultra 7 155H · browser tab uses happy-dom (Chromium), where $ is compared against jQuery 4 · lower is faster.
String
Number
Array
Object
Math
State
Pick your runtime. Koyo meets you there.
npm install koyojsbun add koyojsThen 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";