A small collection of TypeScript-first utilities for everyday application code. Each module is focused, composable, and ships with zero runtime dependencies.
import {
chunk,
isIterable,
isLength,
isPopulatedArray,
partition,
createDeterministicKeySelector,
toArray,
} from "@jossmac/lil-libs/array";Type guard that checks whether a value implements the iterable protocol.
isIterable(new Map()); // true
isIterable(new Set()); // true
isIterable([]); // true
isIterable({}); // falseType guard for narrowing an array to a tuple of a specific length.
const values: number[] = [1, 2, 3];
if (isLength(values, 3)) {
// values: [number, number, number]
}Type guard for narrowing an array to a non-empty tuple-like type.
const values: number[] = [1, 2, 3];
if (isPopulatedArray(values)) {
// values: [number, ...number[]]
}Returns an array for nullish, scalar, iterable, or array input.
toArray(null); // []
toArray(1); // [1]
toArray(new Set([1, 2])); // [1, 2]Splits an array into fixed-size chunks.
The last chunk may be smaller than the given size if the array does not divide evenly.
chunk([1, 2, 3, 4], 2); // [[1, 2], [3, 4]]
chunk([1, 2, 3, 4, 5], 2); // [[1, 2], [3, 4], [5]]
chunk([], 2); // []Splits an array into two arrays using a predicate.
partition([1, 2, 3, 4], (n) => n % 2 === 0);
// [[2, 4], [1, 3]]
partition(["a", "bb", "ccc"], (s) => s.length > 1);
// [["bb", "ccc"], ["a"]]Behaviour:
- Returns a 2-item tuple:
[matched, unmatched]. - Preserves the original item order within both output arrays.
- Passes
(item, index, array)to the predicate.
Creates a function that deterministically maps a string to one of the provided keys using a stable hash.
const colors = ["red", "green", "blue"] as const;
const getColor = createDeterministicKeySelector(colors);
getColor("Albert"); // 'blue'
getColor("Barbara"); // 'green'
getColor("Charlie"); // 'red'
const color = getColor("David");
// ^? 'red' | 'green' | 'blue' (inferred return type)Behaviour:
- Returns the same key for the same input every time.
- Preserves literal key types (for
as constarrays). - Supports empty input strings.
- Throws if called with an empty keys array.
import { assert, assertNever, ensure } from "@jossmac/lil-libs/assert";Asserts that a value is present (or that a boolean is true).
function getName(id: number): string | undefined;
const maybeName = getName(123);
// ^? string | undefined
assert(maybeName, "Name is required");
const name = maybeName;
// ^? stringBehaviour:
- Throws for
false,null, andundefined. - Does not throw for other falsy values like
0and"". - Narrows types after the assertion.
Throws for unreachable branches in discriminated unions.
switch (status.kind) {
case "idle":
case "loading":
case "done":
break;
default:
assertNever(status.kind);
}A convenience wrapper around assert that returns the value if the assertion passes.
function findUser(id: number): User | null;
const user = ensure(findUser(123), "User is required");
// ^? Userimport { errorOnce, warnOnce } from "@jossmac/lil-libs/console";Logs each unique error message only once per runtime instance.
errorOnce("API request failed");
errorOnce("API request failed"); // ignoredLogs each unique warning message only once per runtime instance.
warnOnce("Using fallback value");
warnOnce("Using fallback value"); // ignoredimport { UNICODE_CHARS } from "@jossmac/lil-libs/constants";Common unicode characters used to compose UI text content. Use when you need explicit control over spacing and line-break behavior in UI copy.
`${10}${UNICODE_CHARS.narrowNoBreakSpace}kg`; // "10 kg"
`Page${UNICODE_CHARS.noBreakSpace}1`; // stays on one line
`USD${UNICODE_CHARS.wordJoiner}/${UNICODE_CHARS.wordJoiner}EUR`; // keeps the token together
`super${UNICODE_CHARS.zeroWidthSpace}long`; // invisible optional wrap pointA narrow form of a no-break space, typically the width of a thin or mid space. Use when two tokens should stay together with tighter spacing than a normal space (for example number-unit pairs or compact punctuation spacing).
A space character that prevents an automatic line break at its position. Use when two adjacent words or symbols must remain on the same line while keeping normal space width.
A non-breaking form of the zero-width space. Use when you must prevent a line break between characters without introducing any visible spacing.
Intended for invisible word separation and for line-break control; it has no width. Use when you want to add optional wrap points in long unbroken text without adding visible spaces.
import {
ensureError,
isError,
isErrorLike,
parseError,
} from "@jossmac/lil-libs/error";Guard for native Error instances.
isError(new Error("boom")); // true
isError("boom"); // falseGuard for error-like objects exposing a string message property.
isErrorLike({ message: "boom" }); // true
isErrorLike({ message: 123 }); // falseReturns a human-readable error message from unknown input.
parseError(new Error("Boom")); // "Boom"
parseError("Something went wrong"); // "Something went wrong"
parseError({ message: "from object" }); // "from object"
parseError(null); // "An unknown error occurred."
parseError({ message: 123 }); // "An unknown error occurred."
parseError(null, "Custom fallback"); // "Custom fallback"Behaviour:
- Returns
error.messagefor nativeErrorvalues. - Returns
value.messagefor error-like objects wheremessageis a string. - Returns string inputs as-is.
- Returns a fallback message for all other values.
Returns an Error instance from unknown thrown input.
ensureError(new Error("boom")); // same Error instance
ensureError("boom"); // Error("boom")
ensureError({ message: "boom", name: "CustomError" }); // Error with copied metadataimport {
isDefined,
lazy,
noop,
not,
resolveMaybeFn,
} from "@jossmac/lil-libs/function";Does nothing and returns undefined.
button.addEventListener("click", noop);Type guard for filtering out null and undefined without losing type precision.
const bad = [1, null, 2, undefined, 3].filter(Boolean);
// ^? (number | null | undefined)[]
const good = [1, null, 2, undefined, 3].filter(isDefined);
// ^? number[]Inverts a predicate.
const isEven = (n: number) => n % 2 === 0;
const isOdd = not(isEven);
isOdd(3); // true
isOdd(4); // falseReturns a value directly or by invoking a unary function.
resolveMaybeFn(42); // 42
resolveMaybeFn((x: number) => x * 2, 21); // 42Returns a lazily computed value that is cached after first access. Access the result via .value.
const settings = lazy(() => loadSettings());
settings.value; // computes once
settings.value; // cachedimport { relativeTime } from "@jossmac/lil-libs/datetime";Formats a date as relative time for nearby past or future values and falls back to a date string once the value is 24 hours away or more.
relativeTime(new Date(Date.now() - 1_000 * 60)); // "1 minute ago"
relativeTime(new Date(Date.now() + 1_000 * 60 * 5)); // "in 5 minutes"
relativeTime(new Date(Date.now() - 1_000), { numeric: "auto" }); // "Just now"
relativeTime(new Date(Date.now() - 1_000 * 60), { style: "short" }); // "1 min. ago"
relativeTime(
new Date(Date.now() - 1_000 * 60 * 60 * 24),
{},
{ dateStyle: "medium" },
); // "Jan 6, 2026" (locale-dependent)Signature:
function relativeTime(
value: Date | string,
relativeOptions?: {
numeric?: Intl.RelativeTimeFormatNumeric;
style?: Intl.RelativeTimeFormatStyle;
},
dateOptions?: Intl.DateTimeFormatOptions,
): string;Behaviour:
- Accepts a
Dateor ISO 8601 string. - Returns relative output (e.g.
"1 minute ago"or"in 5 minutes") for past and future values within 24 hours. - Returns a localized date string once the value is 24 hours old or more.
- Uses the same 24-hour cutoff for future dates; after that it falls back to a date string.
- With
numeric: "auto", values within 10 seconds return"Just now". - Throws a
TypeErrorfor invalid date input. - Supports relative formatting via
numericandstyleoptions. - Supports custom date formatting via
Intl.DateTimeFormatoptions.
import {
ariaCurrent,
atScrollBottom,
atScrollLeft,
atScrollRight,
atScrollTop,
getAbsoluteClientRect,
getComputedStyle,
getDisplayMode,
hasScrollX,
hasScrollY,
isHtmlElement,
isKeyboardInput,
isTouchCapable,
isTouchDevice,
joinIds,
nearestComputedStyle,
querySelector,
querySelectorAll,
toDataAttributes,
} from "@jossmac/lil-libs/dom";Type guard for narrowing unknown values to HTMLElement.
const el = document.querySelector("#app");
// ^? Element | null
if (isHtmlElement(el)) {
el.tabIndex = -1; // safe to operate on `el` as an `HTMLElement` type
}Checks whether an event target is an element that can trigger the software keyboard, on mobile devices.
const textInput = document.createElement("input");
textInput.type = "text";
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
isKeyboardInput(textInput); // true
isKeyboardInput(checkbox); // false
isKeyboardInput(document.createElement("textarea")); // trueReturns true when the device can receive touch input.
Some devices support both touch and mouse input, such as laptop computers with a touchscreen.
isTouchCapable(); // true on touch-capable devices, otherwise falseReturns true when the primary pointer is "coarse" (for example, touch input).
isTouchDevice(); // true on coarse-pointer devices, otherwise falseThin wrapper around the Element.querySelector() method, which qualifies the returned value as an HTMLElement.
const container = document.createElement("div");
container.innerHTML = "<button>Save</button><svg><circle /></svg>";
querySelector(container, "button"); // HTMLButtonElement
querySelector(container, "circle"); // null (non-HTMLElement)
querySelector(null, "button"); // nullThin wrapper around the Element.querySelectorAll() method, which returns HTMLElement[] instead of NodeListOf<Element>.
const container = document.createElement("div");
container.innerHTML = "<span>One</span><span>Two</span><svg><circle /></svg>";
querySelectorAll(container, "span"); // [HTMLSpanElement, HTMLSpanElement]
querySelectorAll(container, "span, circle"); // spans only
querySelectorAll(undefined, "span"); // []Returns a computed style value for regular CSS properties and CSS variables.
const el = document.createElement("div");
getComputedStyle(el, "color"); // e.g. "rgb(0, 0, 0)"
getComputedStyle(el, "line-height"); // e.g. "normal"
getComputedStyle(el, "--space-10"); // custom property valueWalks up the parent chain until it finds a non-empty computed style value.
const parent = document.createElement("div");
const child = document.createElement("span");
parent.appendChild(child);
nearestComputedStyle(child, "color");
// value from child if present, otherwise nearest parent valueReturns the current display mode for browser and PWA contexts.
Possible values:
"fullscreen""minimal-ui""picture-in-picture""standalone""window-controls-overlay""browser"
getDisplayMode();
// e.g. "browser" in a tab, "standalone" in an installed PWAReturns the appropriate aria-current value for a nav item based on the current path.
ariaCurrent("/about", "/about"); // "page"
ariaCurrent("/about/team", "/about"); // "true"
ariaCurrent("/contact", "/about"); // "false"
ariaCurrent("/about-us", "/about"); // "false" (not a child match)Behaviour:
- Returns
"page"for exact path matches. - Returns
"true"for child routes (for example/about/teamunder/about). - Returns
"false"for non-matches and null pathnames. - Normalizes trailing slashes (except root) before matching.
Joins IDs for ARIA attributes like aria-labelledby and aria-describedby.
joinIds("title-id", "description-id"); // "title-id description-id"
joinIds("title-id", null, undefined, "", false, "description-id"); // "title-id description-id"
joinIds(null, undefined, "", false); // undefinedBehaviour:
- Filters out falsy values (
null,undefined,false,""). - Returns IDs joined by a single space.
- Returns
undefinedwhen no valid IDs remain.
Converts object keys to HTML data-* attributes.
toDataAttributes({
isSelected: true,
panelIndex: 2,
count: 0,
empty: "",
});
// {
// "data-selected": true,
// "data-panel-index": 2,
// "data-count": 0,
// }
toDataAttributes(
{ isSelected: true, isolated: true, empty: "" },
{ omitFalsyValues: false, trimBooleanKeys: false },
);
// {
// "data-is-selected": true,
// "data-isolated": true,
// "data-empty": "",
// }Behaviour:
- Converts camelCase keys to kebab-case.
- Omits
nullandundefinedvalues. - Omits
falseand""by default. - Preserves
0values, even whenomitFalsyValuesistrue. - Trims leading
isfrom boolean-style keys by default. - Only trims leading
iswhen it is followed by an uppercase letter (isSelected->data-selected,isolated->data-isolated).
Returns getBoundingClientRect() values offset by document scroll, giving absolute page coordinates.
const rect = getAbsoluteClientRect(document.body);
rect.top;
rect.left;
rect.width;
rect.height;Checks whether an element has horizontal overflow.
hasScrollX(document.body); // true or falseChecks whether an element has vertical overflow.
hasScrollY(document.body); // true or falseChecks whether an element is at the top of its scroll range.
Uses <= 0 to handle elastic scrolling behavior.
atScrollTop(document.documentElement); // true or falseChecks whether an element is at the bottom of its scroll range.
Uses >= to handle elastic scrolling behavior.
atScrollBottom(document.documentElement); // true or falseChecks whether an element is at the left edge of its scroll range.
Uses <= 0 to handle elastic scrolling behavior.
atScrollLeft(document.documentElement); // true or falseChecks whether an element is at the right edge of its scroll range.
Uses >= to handle elastic scrolling behavior.
atScrollRight(document.documentElement); // true or falseimport {
stringifyWithBigIntAsString,
stringifyWithSortedKeys,
} from "@jossmac/lil-libs/json";Serialises JSON while converting BigInt values to strings.
JSON.stringify({ id: 123n });
// ⚠ Uncaught TypeError: Do not know how to serialize a BigInt
stringifyWithBigIntAsString({ id: 123n });
// '{"id":"123"}'Serialises deterministic JSON by sorting object keys at every nesting level.
stringifyWithSortedKeys({ b: 2, a: 1 });
// '{"a":1,"b":2}'
stringifyWithSortedKeys([{ z: 1, a: 2 }]);
// '[{"a":2,"z":1}]'Behaviour:
- Object keys are sorted alphabetically.
- Array order is preserved.
undefinedobject properties are omitted.
import {
clamp,
findNearest,
isAscending,
isDescending,
isFiniteNumber,
isNumber,
lerp,
remap,
roundToPrecision,
roundToStep,
sequence,
unlerp,
} from "@jossmac/lil-libs/number";Runtime guard for JavaScript numbers, excluding NaN.
isNumber(42); // true
isNumber(Infinity); // true
isNumber(NaN); // false
isNumber("foo"); // false
isNumber({}); // falseA convenience wrapper around isNumber, that also checks whether the value is finite.
isFiniteNumber(42); // true
isFiniteNumber(Infinity); // falseChecks whether an array is in ascending order (allowing equal neighbouring values).
isAscending([1, 1, 2, 3]); // true
isAscending([3, 2, 1]); // falseChecks whether an array is in descending order (allowing equal neighbouring values).
isDescending([3, 3, 2, 1]); // true
isDescending([1, 2, 3]); // falseConstrains a number to an inclusive range.
clamp(5, 0, 10); // 5
clamp(-5, 0, 10); // 0
clamp(15, 0, 10); // 10Rounds a number to a specified number of fractional digits.
roundToPrecision(3.14159, 2); // 3.14
roundToPrecision(3.005, 2); // 3.01Signature:
function roundToPrecision(value: number, digits: number, base?: number): number;Behaviour:
digits <= 0behaves likeMath.round().basedefaults to10; most callers should leave it unchanged.
Rounds a number to the nearest step interval.
roundToStep(5.26, 0.25); // 5.25
roundToStep(-5.26, 0.25); // -5.25Behaviour:
- Throws for
step = 0. - Throws for non-finite step values like
InfinityandNaN.
Returns the closest value from a list, with configurable tie-breaking.
const items = [1, 3, 5, 7, 9];
findNearest(4, items); // 3 (default bias: "first")
findNearest(4, items, "last"); // 5
findNearest(4, items, "smaller"); // 3
findNearest(4, items, "larger"); // 5Bias options:
"first"/"last"— prefer the item that appears earlier or later in the array."smaller"/"larger"— prefer the numerically smaller or larger tied value.- Throws if
itemsis empty.
Generates inclusive numeric sequences in ascending or descending order.
sequence(1, 5); // [1, 2, 3, 4, 5]
sequence(5, 1); // [5, 4, 3, 2, 1]
sequence(0, 1, 0.33); // [0, 0.33, 0.66, 0.99]Behaviour:
- Includes both start and end when reachable by step increments.
- Supports negative step input (uses absolute step size).
- Derives decimal precision from the provided step.
- Throws for
step = 0or non-finite step values.
Linear interpolation between two values.
lerp(0, 100, 0.25); // 25Inverse interpolation that returns a clamped factor in the 0..1 range.
unlerp(0, 100, 25); // 0.25
// unlerp clamps outside-range values to 0..1
unlerp(0, 10, -5); // 0
unlerp(0, 10, 15); // 1Maps a value from one numeric range to another using linear interpolation.
remap(5, [0, 10], [0, 100]); // 50
remap(-5, [-10, 0], [0, 100]); // 50
remap(7.5, [0, 10], [-20, -10]); // -12.5
remap(-5, [0, 10], [0, 100]); // 0 (clamped)
remap(15, [0, 10], [0, 100]); // 100 (clamped)
remap(15, [0, 10], [0, 100], { clamp: false }); // 150Behaviour:
- Clamps to the output range by default.
- Supports negative and floating-point ranges.
- Allows extrapolation with
{ clamp: false }. - Handles degenerate input ranges (
from === to) predictably.
import {
TObject,
isPlainObject,
typedEntries,
typedFromEntries,
typedKeys,
} from "@jossmac/lil-libs/object";Checks whether a value is a plain object (including Object.create(null)).
isPlainObject({}); // true
isPlainObject(Object.create(null)); // true
isPlainObject([]); // false
isPlainObject(new Date()); // falseTyped alternative to Object.keys() that preserves key inference.
const obj = { foo: 1, bar: "hello" };
const keys = typedKeys(obj);
// ^? ("foo" | "bar")[]Typed alternative to Object.entries() that preserves key/value tuples.
const obj = { foo: 1, bar: "hello" };
const entries = typedEntries(obj);
// ^? (["foo", number] | ["bar", string])[]Typed alternative to Object.fromEntries() that preserves output shape.
const entries = [
["foo", 1],
["bar", "hello"],
] as const;
const rebuilt = typedFromEntries(entries);
// ^? { foo: number; bar: string }Provides a namespace-like wrapper around the typed object helpers.
const keys = TObject.keys({ foo: 1, bar: "hello" });
// ^? ("foo" | "bar")[]Exports a single random object containing all methods to avoid naming collisions.
import { random } from "@jossmac/lil-libs/random";Returns random boolean values.
random.bool(); // true or falseGenerates random integers in an inclusive range.
random.int(1, 3); // 1, 2, or 3
random.int(10, 1); // still validBehaviour:
random.int(min, max)is inclusive of both bounds.- Reversed bounds are automatically normalised.
Generates random floating-point numbers in a half-open range.
random.float(10, 20); // 10 <= n < 20
random.float(20, 10); // still validBehaviour:
random.float(min, max)is inclusive ofminand exclusive ofmax.- Reversed bounds are automatically normalised.
Returns one random item from an array.
random.choice(["a", "b", "c"]); // one itemReturns a randomly sampled subset without mutating the original array.
const items = ["a", "b", "c", "d"];
random.sample(items, 2); // e.g. ["d", "a"]
random.sample(items); // single-item sample
random.sample(items, 0); // []
items; // still ["a", "b", "c", "d"]Behaviour:
- Defaults to
count = 1. - Returns an empty array when
countis0. - Returns all items when
countequals the array length. - Never mutates the input array.
Returns a shuffled copy without mutating the original array.
const items = [1, 2, 3, 4];
random.shuffle(items); // shuffled copy
items; // unchangedCreates a function that returns a newly shuffled copy on each call.
const items = [1, 2, 3, 4];
const shuffleNow = random.shuffler(items);
shuffleNow(); // shuffled copy each callCreates a function that returns a random sample on each call.
const items = [1, 2, 3, 4];
const sampleTwo = random.sampler(items, 2);
sampleTwo(); // random 2-item sample each callimport {
base64Encode,
contains,
formatInitials,
isString,
pluralize,
} from "@jossmac/lil-libs/string";Type guard for string values.
isString("hello"); // true
isString(123); // falseEncodes UTF-8 strings to base64, or to a base64 data URI when a MIME type is provided.
base64Encode("hello");
// "aGVsbG8="
base64Encode("hello", "text/plain");
// "data:text/plain;base64,aGVsbG8="Behaviour:
- Supports Unicode input.
- Preserves MIME type in data URI output.
- Optimises SVG payload whitespace when
mimeTypeis"image/svg+xml".
Case-insensitive and diacritic-insensitive substring matching.
contains("café", "cafe"); // true
contains("Hello World", "world"); // true
contains("hello", ""); // true
contains("hello", "bye"); // falseSignature:
function contains(string: string, substring: string, locale?: string): boolean;Behaviour:
- Uses
Intl.Collatorwith accent-insensitive and case-insensitive matching. - Accepts an optional
locale, which defaults to"en". - Returns
truefor an empty substring.
Returns singular/plural forms with optional count prefix.
pluralize(1, "wallet"); // "1 wallet"
pluralize(2, "wallet"); // "2 wallets"
pluralize(2, ["person", "people"]); // "2 people"
pluralize(2, ["person", "people"], false); // "people"Returns initials for names with Unicode-aware grapheme support.
formatInitials("John Doe"); // "JD"
formatInitials("John Henry Doe"); // "JD"
formatInitials("John Henry Doe", { maxLetters: 3 }); // "JHD"
formatInitials("John Ronald Reuel Tolkien"); // "JT"
formatInitials("John Ronald Reuel Tolkien", { maxLetters: 3 }); // "JRR"
formatInitials("Élodie Durand"); // "ÉD"
formatInitials("ilker", { locale: "tr" }); // "İL"
formatInitials("李小龍"); // "李小"Signature:
function formatInitials(
name: string,
options?: {
maxLetters?: number;
locale?: string;
},
): string;Behaviour:
- Defaults to
maxLetters = 2. - Uses the first letter from the first word and the first letter from the last word when
maxLetters === 2. - Uses up to one letter from each word, left to right, when
maxLetters >= 3. - For single-word names, uses the first
maxLettersletters. - Returns
"?"for empty or whitespace-only input. - Throws when
maxLettersis not finite or is less than1.
import type {
Maybe,
NonNullableValues,
Prettify,
Satisfies,
SomeOptional,
SomeRequired,
TupleOf,
UnknownRecord,
Widen,
} from "@jossmac/lil-libs/types";Represents a maybe-present value for app-level checks.
type MaybeName = Maybe<string>;
// ^? string | null | undefinedFlattens intersections and mapped types into a cleaner displayed shape.
type Raw = { id: string } & { name: string };
type User = Prettify<Raw>;
// ^? { id: string; name: string }Constrains T to be assignable to Base while preserving T's full detail.
type Endpoint = Satisfies<
{ method: "GET"; path: "/users" },
{ method: "GET" | "POST"; path: string }
>;
// ^? { method: "GET"; path: "/users" }Removes null and undefined from each property value type.
type Input = { id: string | null; age?: number | undefined };
type Output = NonNullableValues<Input>;
// ^? { id: string; age?: number }Makes a subset of keys required while leaving all other keys unchanged.
type Input = { id?: string; name?: string; active?: boolean };
type Output = SomeRequired<Input, "id">;
// ^? { id: string; name?: string; active?: boolean }Makes a subset of keys optional while leaving all other keys unchanged.
type Input = { id: string; name: string; active: boolean };
type Output = SomeOptional<Input, "active">;
// ^? { id: string; name: string; active?: boolean }Builds a fixed-length tuple of N elements of type T.
type Triple = TupleOf<number, 3>;
// ^? [number, number, number]Widens literals to their broader primitive types.
type A = Widen<"hello">;
// ^? string
type B = Widen<42>;
// ^? numberAlias for a generic object map with unknown values.
type Payload = UnknownRecord;
// ^? Record<string, unknown>Prerequisites:
- Node.js
24(active LTS) - pnpm
10.33.0(managed via Corepack or installed globally)
nvm use
pnpm installpnpm check # run all static checks
pnpm check:types # TypeScript
pnpm check:lint # ESLint
pnpm check:format # Prettier
pnpm test # run tests once
pnpm test:watch # watch mode
pnpm test:coverage # run tests with v8 coverage
pnpm release patch # bump patch, create tag, push commit + tag
pnpm release minor # bump minor, create tag, push commit + tag
pnpm release major # bump major, create tag, push commit + tag
pnpm release patch -- --no-push # keep release commit + tag localWhen ready:
pnpm publish --access public