Sitelet https://github.com/EroyEroy/utils
Skip to content

Repository files navigation

@eroyeroy/utils

A fully typed TypeScript utility library with tree-shakeable subpath imports and zero runtime dependencies.

Installation

npm

npm i @eroyeroy/utils

pnpm

pnpm add @eroyeroy/utils

yarn

yarn add @eroyeroy/utils

bun

bun add @eroyeroy/utils

Usage

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"

Modules

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

Array

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 }]

Guards

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]

String

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

Object

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 }

Number

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

Async

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

Function

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]

URL

import { ... } from "@eroyeroy/utils/url"
Function Description
parseurl(/sitelet?url=https%3A%2F%2Fgithub.com%2FEroyEroy%2Furl) Parses to URL or returns null on failure
buildurl(/sitelet?url=https%3A%2F%2Fgithub.com%2FEroyEroy%2Fbase%2C%2520params) 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(/sitelet?url=https%3A%2F%2Fgithub.com%2FEroyEroy%2Furl) Returns true for valid absolute URLs
joinurl(/sitelet?url=https%3A%2F%2Fgithub.com%2FEroyEroy%2Fbase%2C%2520path) 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"

Format

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"

Date

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

Validation

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

Documentation

Generate the full HTML API docs with:

pnpm docs

Output is written to ./docs/index.html.

Development

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

License

MIT

About

A fully typed TypeScript utility library with tree-shakeable subpath imports and zero runtime dependencies.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages