A fully typed TypeScript utility library with tree-shakeable subpath imports and zero runtime dependencies.
npm
npm i @eroyeroy/utils
pnpm
pnpm add @eroyeroy/utils
yarn
yarn add @eroyeroy/utils
bun
bun add @eroyeroy/utils
Import from the root or from any subpath:
// Full import
import { groupBy, isEmail, formatBytes } from "@eroyeroy/utils"
// Subpath import (smallest bundle)
import { groupBy } from "@eroyeroy/utils/array"
import { isEmail } from "@eroyeroy/utils/validation"
import { formatBytes } from "@eroyeroy/utils/format"
| Module | Subpath | Functions |
|---|---|---|
| Array | @eroyeroy/utils/array |
34 |
| Guards | @eroyeroy/utils/guards |
14 |
| String | @eroyeroy/utils/string |
11 |
| Object | @eroyeroy/utils/object |
9 |
| Number | @eroyeroy/utils/number |
10 |
| Async | @eroyeroy/utils/async |
4 |
| Function | @eroyeroy/utils/function |
6 |
| URL | @eroyeroy/utils/url |
7 |
| Format | @eroyeroy/utils/format |
4 |
| Date | @eroyeroy/utils/date |
16 |
| Validation | @eroyeroy/utils/validation |
10 |
import { ... } from "@eroyeroy/utils/array"
arrayIncludes(array, value)Typed Array.prototype.includes. Narrows the type of value to T[number] on true.
const roles = ["admin", "user"] as const
arrayIncludes(roles, "admin") // => true, typed as "admin" | "user"
averageBy(array, key)Average of a numeric key. Returns 0 for empty arrays.
averageBy([{ score: 10 }, { score: 20 }, { score: 30 }], "score") // => 20
chunkArray(array, size)Splits into chunks of at most size elements.
chunkArray([1, 2, 3, 4, 5], 2) // => [[1, 2], [3, 4], [5]]
compactArray(array)Removes null and undefined.
compactArray([1, null, 2, undefined, 3]) // => [1, 2, 3]
countBy(array, key)Counts occurrences of each value of key.
countBy([{ role: "admin" }, { role: "user" }, { role: "admin" }], "role")
// => { admin: 2, user: 1 }
dropArray(array, count)Removes the first count elements.
dropArray([1, 2, 3, 4], 2) // => [3, 4]
findArrayItemByKey(array, key, value)Finds the first item where item[key] === value.
findArrayItemByKey([{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }], "id", 2)
// => { id: 2, name: "Bob" }
firstArrayItem(array)Returns the first element (typed as non-undefined for non-empty tuples).
firstArrayItem([1, 2, 3]) // => 1
firstArrayItem([]) // => undefined
flattenArray(array)Flattens one level of nesting.
flattenArray([[1, 2], [3, 4], [5]]) // => [1, 2, 3, 4, 5]
groupBy(array, key)Groups items by the value of a key.
groupBy([{ role: "admin", name: "Alice" }, { role: "user", name: "Bob" }], "role")
// => { admin: [{ role: "admin", name: "Alice" }], user: [{ role: "user", name: "Bob" }] }
hasArrayItemByKey(array, key, value)Returns true if any item matches item[key] === value.
hasArrayItemByKey([{ id: 1 }, { id: 2 }], "id", 2) // => true
insertArrayItem(array, item, index?)Inserts at index (defaults to end).
insertArrayItem([1, 2, 4], 3, 2) // => [1, 2, 3, 4]
isArray(value)Type guard for arrays.
isArray([1, 2, 3]) // => true
isEmptyArray(value)Returns true if value is an array with zero elements.
isEmptyArray([]) // => true
isFilledArray(value)Returns true if value is an array with at least one element.
isFilledArray([1]) // => true
lastArrayItem(array)Returns the last element (typed as non-undefined for non-empty tuples).
lastArrayItem([1, 2, 3]) // => 3
mapArrayToRecord(array, key)Transforms an array to a record keyed by key.
mapArrayToRecord([{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }], "id")
// => { 1: { id: 1, name: "Alice" }, 2: { id: 2, name: "Bob" } }
maxBy(array, key)Returns the item with the maximum value of key.
maxBy([{ age: 25 }, { age: 30 }, { age: 20 }], "age") // => { age: 30 }
minBy(array, key)Returns the item with the minimum value of key.
minBy([{ age: 25 }, { age: 30 }, { age: 20 }], "age") // => { age: 20 }
moveArrayItem(array, fromIndex, toIndex)Moves an item to a new position.
moveArrayItem([1, 2, 3, 4], 0, 3) // => [2, 3, 4, 1]
partitionArray(array, predicate)Splits into [matched, unmatched].
partitionArray([1, 2, 3, 4, 5], n => n % 2 === 0) // => [[2, 4], [1, 3, 5]]
rangeArray(end) / rangeArray(start, end)Creates a range of integers.
rangeArray(5) // => [0, 1, 2, 3, 4]
rangeArray(1, 5) // => [1, 2, 3, 4, 5]
rangeArray(5, 1) // => [5, 4, 3, 2, 1]
removeArrayItem(array, item)Removes all occurrences of an item by reference.
removeArrayItem([1, 2, 3, 2], 2) // => [1, 3]
removeArrayItemByKey(array, key, value)Removes all items where item[key] === value.
removeArrayItemByKey([{ id: 1 }, { id: 2 }, { id: 1 }], "id", 1)
// => [{ id: 2 }]
replaceArrayItem(array, itemToReplace, replacement)Replaces all items equal by reference.
replaceArrayItem([1, 2, 3, 2], 2, 99) // => [1, 99, 3, 99]
replaceArrayItemByKey(array, key, value, replacement)Replaces all items matching item[key] === value. Accepts a function.
replaceArrayItemByKey([{ id: 1, x: 0 }], "id", 1, item => ({ ...item, x: 99 }))
// => [{ id: 1, x: 99 }]
shuffleArray(array, random?)Fisher-Yates shuffle. Accepts a custom RNG for deterministic results.
shuffleArray([1, 2, 3, 4, 5]) // => e.g. [3, 1, 5, 2, 4]
sortBy(array, key, direction?)Sorts by key ("asc" or "desc"). Supports strings, numbers, booleans, and Dates.
sortBy([{ age: 30 }, { age: 20 }, { age: 25 }], "age") // ascending
sortBy([{ age: 30 }, { age: 20 }, { age: 25 }], "age", "desc") // descending
sumBy(array, key)Sum of a numeric key.
sumBy([{ price: 10 }, { price: 20 }, { price: 30 }], "price") // => 60
takeArray(array, count)Returns the first count elements.
takeArray([1, 2, 3, 4], 2) // => [1, 2]
toggleArrayItem(array, item)Removes the item if present, appends if absent. Useful for selection sets.
toggleArrayItem([1, 2, 3], 2) // => [1, 3]
toggleArrayItem([1, 3], 2) // => [1, 3, 2]
uniqueArray(array)Removes duplicate values, preserving insertion order.
uniqueArray([1, 2, 1, 3, 2]) // => [1, 2, 3]
uniqueBy(array, key)Removes duplicates based on a key, keeping the first occurrence.
uniqueBy([{ id: 1, v: "a" }, { id: 1, v: "b" }, { id: 2, v: "c" }], "id")
// => [{ id: 1, v: "a" }, { id: 2, v: "c" }]
updateArrayItemByKey(array, key, value, update)Updates all items matching item[key] === value. Accepts a partial object or function.
updateArrayItemByKey([{ id: 1, x: 0 }, { id: 2, x: 0 }], "id", 1, { x: 99 })
// => [{ id: 1, x: 99 }, { id: 2, x: 0 }]
updateArrayItemByKey([{ id: 1, x: 5 }], "id", 1, item => ({ ...item, x: item.x * 2 }))
// => [{ id: 1, x: 10 }]
import { ... } from "@eroyeroy/utils/guards"
| Function | Description |
|---|---|
isString(value) |
value is string |
isNumber(value) |
value is number (excludes NaN) |
isBoolean(value) |
value is boolean |
isNull(value) |
value is null |
isUndefined(value) |
value is undefined |
isNullish(value) |
value is null | undefined |
isDefined(value) |
value is T — useful for array.filter(isDefined) |
isObject(value) |
Plain object (not null, not array) |
isFunction(value) |
value is (...args) => unknown |
isDate(value) |
value instanceof Date |
isError(value) |
value instanceof Error |
isSymbol(value) |
value is symbol |
isBigInt(value) |
value is bigint |
isRegExp(value) |
value instanceof RegExp |
[1, null, 2, undefined].filter(isDefined) // => [1, 2]
import { ... } from "@eroyeroy/utils/string"
| Function | Description | Example |
|---|---|---|
capitalize(str) |
Uppercases the first character | "hello" → "Hello" |
camelCase(str) |
Converts to camelCase |
"hello_world" → "helloWorld" |
snakeCase(str) |
Converts to snake_case |
"helloWorld" → "hello_world" |
kebabCase(str) |
Converts to kebab-case |
"HelloWorld" → "hello-world" |
pascalCase(str) |
Converts to PascalCase |
"hello-world" → "HelloWorld" |
truncate(str, max, suffix?) |
Truncates to max chars |
truncate("Hello, World!", 8) → "Hello..." |
slugify(str) |
URL-friendly slug | "Hello, World!" → "hello-world" |
reverseString(str) |
Reverses characters | "hello" → "olleh" |
maskString(str, visible?, char?) |
Masks all but last N chars | maskString("1234567890") → "******7890" |
countWords(str) |
Counts whitespace-separated words | "hello world" → 2 |
isEmptyString(str) |
True if empty or only whitespace | "" / " " → true |
isFilledString(str) |
True if has non-whitespace content | "hi" → true |
import { ... } from "@eroyeroy/utils/object"
| Function | Description |
|---|---|
pick(obj, keys) |
Returns a new object with only the specified keys |
omit(obj, keys) |
Returns a new object with the specified keys excluded |
objectKeys(obj) |
Typed Object.keys |
objectValues(obj) |
Typed Object.values |
objectEntries(obj) |
Typed Object.entries |
mapValues(obj, fn) |
Maps values, preserving keys |
filterValues(obj, predicate) |
Filters entries by value |
isEmptyObject(obj) |
True if no own enumerable keys |
isFilledObject(obj) |
True if at least one own enumerable key |
pick({ a: 1, b: 2, c: 3 }, ["a", "c"]) // => { a: 1, c: 3 }
omit({ a: 1, b: 2, c: 3 }, ["b"]) // => { a: 1, c: 3 }
mapValues({ a: 1, b: 2 }, v => v * 2) // => { a: 2, b: 4 }
filterValues({ a: 1, b: 2, c: 3 }, v => v > 1) // => { b: 2, c: 3 }
import { ... } from "@eroyeroy/utils/number"
| Function | Description | Example |
|---|---|---|
clamp(value, min, max) |
Clamps to [min, max] |
clamp(15, 0, 10) → 10 |
inRange(value, min, max) |
Checks inclusive range | inRange(5, 0, 10) → true |
roundTo(value, precision) |
Rounds to N decimal places | roundTo(1.2345, 2) → 1.23 |
isInteger(value) |
True if safe integer | isInteger(42) → true |
isFloat(value) |
True if finite non-integer | isFloat(1.5) → true |
isPositive(value) |
True if > 0 |
isPositive(1) → true |
isNegative(value) |
True if < 0 |
isNegative(-1) → true |
isZero(value) |
True if === 0 |
isZero(0) → true |
randomBetween(min, max) |
Random float in [min, max) |
randomBetween(1, 10) |
percentageOf(value, total) |
Calculates percentage | percentageOf(1, 4) → 25 |
import { ... } from "@eroyeroy/utils/async"
sleep(ms)Pauses for ms milliseconds.
await sleep(1000)
retry(fn, options?)Retries an async function on failure with optional exponential backoff.
const data = await retry(() => fetch("/api").then(r => r.json()), {
retries: 3,
delay: 200,
backoff: 2,
})
withTimeout(promise, ms)Rejects with an error if the promise doesn't resolve within ms.
const result = await withTimeout(fetch("/api"), 5000)
parallel(tasks, concurrency?)Runs async tasks with an optional concurrency limit. Preserves result order.
const results = await parallel([
() => fetchUser(1),
() => fetchUser(2),
() => fetchUser(3),
], 2) // at most 2 concurrent requests
import { ... } from "@eroyeroy/utils/function"
debounce(fn, wait)Delays invocation until wait ms have passed since the last call.
const handleInput = debounce((value: string) => search(value), 300)
throttle(fn, limit)Limits invocation to at most once per limit ms.
const handleScroll = throttle(() => updatePosition(), 100)
memoize(fn)Caches results by serialized arguments.
const expensiveCalc = memoize((n: number) => n * n)
expensiveCalc(4) // computed
expensiveCalc(4) // cached
once(fn)Calls fn only on the first invocation; returns the cached result thereafter.
const init = once(() => createConnection())
init() // creates connection
init() // returns cached connection
noop()Does nothing. Useful as a default callback.
element.addEventListener("click", noop)
identity(value)Returns the value unchanged. Useful as a default transform or selector.
[1, 2, 3].map(identity) // => [1, 2, 3]
import { ... } from "@eroyeroy/utils/url"
| Function | Description |
|---|---|
parseUrl(url) |
Parses to URL or returns null on failure |
buildUrl(base, params) |
Appends query params (skips null/undefined values) |
getQueryParam(url, param) |
Gets a single query param or null |
setQueryParam(url, param, value) |
Sets/replaces a query param |
removeQueryParam(url, param) |
Removes a query param |
isValidUrl(url) |
Returns true for valid absolute URLs |
joinUrl(base, path) |
Joins URL and path, normalizing slashes |
buildUrl("https://api.example.com", { page: 1, q: "hello", sort: null })
// => "https://api.example.com/?page=1&q=hello"
getQueryParam("https://example.com?q=hello", "q") // => "hello"
joinUrl("https://example.com/api/", "/users") // => "https://example.com/api/users"
import { ... } from "@eroyeroy/utils/format"
formatBytes(bytes, precision?)formatBytes(0) // => "0 B"
formatBytes(1024) // => "1.00 KB"
formatBytes(1048576, 1) // => "1.0 MB"
formatDuration(ms)formatDuration(500) // => "500ms"
formatDuration(5000) // => "5s"
formatDuration(90000) // => "1m 30s"
formatDuration(3660000) // => "1h 1m"
formatRelativeTime(date, now?)formatRelativeTime(new Date(Date.now() - 5 * 60 * 1000)) // => "5 minutes ago"
formatRelativeTime(new Date(Date.now() + 24 * 60 * 60 * 1000)) // => "in 1 day"
formatPlural(count, singular, plural?)formatPlural(1, "item") // => "item"
formatPlural(3, "item") // => "items"
formatPlural(2, "person", "people") // => "people"
import { ... } from "@eroyeroy/utils/date"
All functions return new Date objects and do not mutate inputs.
| Function | Description |
|---|---|
isValidDate(value) |
Type guard — Date instance with a valid time |
addDays(date, days) |
Adds days (negative to subtract) |
addMonths(date, months) |
Adds months |
addYears(date, years) |
Adds years |
diffInDays(dateA, dateB) |
Whole days from A to B (negative if B < A) |
diffInMonths(dateA, dateB) |
Whole months from A to B |
startOfDay(date) |
00:00:00.000 of the same day (local time) |
endOfDay(date) |
23:59:59.999 of the same day (local time) |
startOfMonth(date) |
First day of the month at midnight |
endOfMonth(date) |
Last moment of the month |
isSameDay(dateA, dateB) |
True if same calendar day |
isBefore(date, referenceDate) |
True if date < referenceDate |
isAfter(date, referenceDate) |
True if date > referenceDate |
isToday(date) |
True if today |
isPast(date) |
True if in the past |
isFuture(date) |
True if in the future |
addDays(new Date("2024-01-01"), 7) // => 2024-01-08
diffInDays(new Date("2024-01-01"), new Date("2024-01-08")) // => 7
isBefore(new Date("2024-01-01"), new Date("2024-06-01")) // => true
import { ... } from "@eroyeroy/utils/validation"
| Function | Description |
|---|---|
isEmail(value) |
Simple local@domain.tld format check |
isUuid(value) |
UUID v1–v5 |
isNumericString(value) |
String that represents a valid number |
isAlpha(value) |
Only a–z / A–Z characters |
isAlphaNumeric(value) |
Only a–z, A–Z, 0–9 characters |
hasMinLength(value, min) |
String length ≥ min |
hasMaxLength(value, max) |
String length ≤ max |
isHexColor(value) |
#rgb or #rrggbb CSS hex color |
isIpv4(value) |
Valid IPv4 address |
isIpv6(value) |
Valid IPv6 address |
isEmail("user@example.com") // => true
isUuid("550e8400-e29b-41d4-a716-446655440000") // => true
isHexColor("#1A2B3C") // => true
isIpv4("192.168.1.1") // => true
isIpv6("2001:0db8:85a3:0000:0000:8a2e:0370:7334") // => true
Generate the full HTML API docs with:
pnpm docs
Output is written to ./docs/index.html.
pnpm install # install dependencies
pnpm test # run tests
pnpm test:watch # run tests in watch mode
pnpm lint # lint source
pnpm lint:fix # lint and auto-fix
pnpm format # format with Prettier
pnpm check-types # TypeScript type check
pnpm build # build ESM + CJS bundles
pnpm ci # lint + type-check + test + build
MIT