Changelog
All notable changes to @mongez/reinforcements are documented here. The format follows Keep a Changelog and this project follows Semantic Versioning.
[3.3.0] 2026-06-17 Added (13) · Tests (1)
Added
Objects
when(condition, value)— conditional object construction for inline spreading. Returnsvaluewhenconditionis truthy, otherwise{}, so a key is only present when the condition holds:{ ...when(isAdmin, { role: "admin" }) }. The value may be a factory function, invoked lazily and only when the condition is truthy. Cleaner than...(condition && { … }), which spreadsfalsewhen the condition fails.
Arrays
partition(array, predicate)— split an array into[pass, fail]by a predicate, in a single pass, preserving order.keyBy(array, key | selector)— index an array into an object keyed by a dot-notation path or a selector function; last item wins on key collision.intersection(...arrays)— unique values present in every array (set intersection), ordered by the first array.difference(array, ...others)— unique values from the first array that appear in none of the others (set difference).union(...arrays)— unique values across all arrays, preserving first-seen order (set union).zip(...arrays)/unzip(rows)— pair arrays into index-aligned tuples and the inverse; the longest input wins, with missing slots filled byundefined.
Numbers
formatDuration(ms, options?)— human-readable duration from milliseconds (3661000→"1h 1m 1s"). Options:units(cap the number of parts),long("1 minute 30 seconds"),separator.ordinal(value, options?)— English ordinal for an integer (1→"1st",22→"22nd"), handling the 11–13 exception and negatives.{ withNumber: false }returns just the suffix.
Async
poll(fn, options?)— repeatedly callfnuntiluntil(result)is truthy (default: result is truthy), then resolve with that result. Supportsinterval,timeout,attempts, andAbortSignal. Built for job-status / readiness polling.waitFor(condition, options?)— resolve once a booleanconditionbecomes truthy; a thinpollwrapper. ExportsWaitForOptions/PollOptionstypes.pReduce(items, reducer, initial)— sequential async reduce, awaiting the accumulator between steps. The async sibling ofArray.prototype.reduce.
Functions
attempt(fn, fallback?)— runfnand return its result; on throw or rejection returnfallback(orundefined). Works for both sync and async functions — a promise-returningfnyields a promise.
All additions are standalone, tree-shakeable, and non-breaking.
Tests
- 53 new tests across 13 spec files (
when,partition,keyBy,intersection,difference,union,zip,unzip,ordinal,formatDuration,poll,waitFor,pReduce,attempt). Suite total: 110 files / 441 tests passing.
[3.2.0] 2026-05-29 Added (6) · Tests (1)
Added
retry—shouldRetrypredicate. Bail out of the retry loop early on non-retryable errors (4xx / validation). Returningfalsestops immediately and throws the current error, with no further attempts or delay. Async-capable. Runs afteronError(observe, then decide).retry— jitter.jitter: "full" | "equal" | truerandomises each delay ("full"=random(0, delay),"equal"=delay/2 + random(0, delay/2)) to avoid thundering herd. Draws from the seedableRandom, so schedules stay reproducible underRandom.seed(n).retry—maxDelay. Caps the computed delay (after backoff, before jitter) so exponential backoff can’t grow without bound.retry— function-formbackoff.backoff: (attempt, baseDelay) => numberfor custom strategies (decorrelated jitter, Fibonacci, capped-exponential) beyond the built-in"linear" | "exponential".retry—AbortSignalsupport. Passsignalto cancel between or during attempts; a pending delay is raced against the signal so cancellation resolves promptly.retryable(fn, options?)— new export. Pre-binds retry options to a function, returning a reusable wrapper.
All additions are optional with defaults preserving the previous behaviour — no breaking change.
Tests
retry.test.tsexpanded to 14 tests covering each option, seeded-jitter determinism, abort timing, and asyncshouldRetry.
[3.1.0] 2026-05-23 Added (2) · Tests (1)
Added
compact(value, options?)— strip “empty” entries from an object or array. Default predicate dropsnull,undefined, and""; keeps0/false/NaN. Empty arrays/objects are dropped, and recursion is enabled by default — parents that become empty after recursion are themselves dropped. Customisable via{ predicate, empties, deep }. Typical use: cleaning API payloads, query strings, form data.pProps(object)— parallel object destructuring (modelled on Bluebird’sPromise.props). Resolves an object of promises to an object of unwrapped values with the same keys, with full type inference. Non-promise values pass through. Joins the existingpAll/pMap/pSeries/pFilterasync family.
Tests
- 96 test files, 372 tests, all passing (+2 files / +18 tests over 3.0.0).
[3.0.0] 2026-05-17 Breaking (17) · Added (59)
Breaking
- Tests migrated from Jest to Vitest. Internal change for contributors; consumers are unaffected.
jest/ts-jest/@types/jest/jest-esm-jsx-transformremoved from devDependencies. only→pick,except→omit(community-standard names).onlyandexceptremain exported as@deprecatedaliases and will be removed in a future major.flattensignature changed from positional args(obj, separator?, keepNested?, parent?, root?)to options-object(obj, { separator?, keepNested?, maxDepth? }). Internal recursion args (parent,root) are no longer part of the public API.mergeis now sane around nullish inputs (merge(null, { a: 1 })returns{ a: 1 }, notnull) and supports array merging strategy via final{ arrays: "replace" | "concat" | "union" }options.getno longer returns the raw literal key when an object happens to have a key matching the dot-notation path; it always walks the path. Falsy values resolve as themselves rather than triggering the default.setnow creates arrays (rather than objects) when an intermediate segment is missing and the next segment is a numeric index.sortnow operates only on plain objects (matches its actual scope; arrays of objects are out of scope — use a collections-packagesortBy).areEqualis a real non-mutating deep-equal. Arrays now respect order ([1,2,3]≠[3,2,1]).shuffleuses Fisher–Yates and is non-mutating by default; pass{ mutate: true }for in-place.clonehandlesRegExp,Error,Map,Set, typed arrays, and circular references via WeakMap memo. Class instances are still preserved by reference (cloning custom constructors is out of scope).toCamelCase/toSnakeCase/toKebabCase/toStudlyCaserebuilt on a sharedwords()normalizer. Fixes the acronym-eating bug:toSnakeCase("AIAgent")is now"ai_agent"(previously"agent").XMLHttpRequest,parseURL,IOError, etc. now produce the expected output.roundactually rounds (wasMath.floorpreviously).debouncenow exposescancel(),flush(),pending(), and supports{ leading, trailing, maxWait }. Removed dead module-leveltimeoutId.Random.color()pads to six hex digits (no more#fffcollapse).Random.date()signature is now{ min?, max? }options.Random.integer/Random.booleanduplicate aliases removed (useint/bool).ARABIC_PATTERNrenamed toARABIC_REGEX;ARABIC_PATTERNkept as deprecated re-export.
Added
Types
Path<T>,PathValue<T, P>— typed dot-notation autocomplete.DeepPartial,DeepRequired,DeepReadonly,DeepMutable.Prettify,UnionToIntersection,Branded.Nullable<T>,Maybe<T>,Awaitable<T>,NonEmptyArray<T>.
Object
pick,omit(new canonical names foronly/except).has(obj, path)— counterpart toget/set.mapValues,mapKeys.entries,fromEntries,keys,values— typed wrappers.invert(obj)— swap keys and values.defaults(target, ...sources)— fill undefined keys.walk(obj, visitor)— recursive leaf traversal.diff(a, b)—{ added, removed, changed }structural diff.freeze(obj)— recursiveObject.freeze.
String
words(str)— shared semantic tokenizer powering all casings.toPascalCase(alias oftoStudlyCasewith the common name).toTitleCase,toConstantCase,toDotCase,toPathCase.slugify(str, options)— URL-safe, diacritic-stripping.truncate(str, length, { suffix, byWord, position }).pad,padStart,padEnd— typed helpers.escapeHtml,unescapeHtml.mask(str, { start, end, char })— middle-masking.template(str, vars)—{path}interpolation with dot-notation.wordCount,charCount(unicode-aware option).reverse(str)— unicode-safe.containsArabic.
Number
ceil,floorwith precision (companions to fixedround).clamp(value, min, max).inRange(value, min, max, options).lerp(a, b, t).toFixed(value, precision)— returns number, not string.formatBytes(bytes, { decimals, binary }).formatNumber(value, { locale, ...IntlOptions }).percentage(value, total, decimals).safeDivide(a, b, fallback).parseNumber(value, fallback).
Function / Utils
throttle(fn, wait, options)— leading/trailing controls.memoize(fn, { resolver, ttl }).once,after(n, fn),before(n, fn).pipe(value, ...fns),compose(...fns).tap(value, fn)andtap.with(fn).curry,partial,partialRight.noop,identity,constant,negate.
Async (new namespace)
sleep(ms, value?).retry(fn, { attempts, delay, backoff, onError }).timeout(promise, ms, message?).pAll,pAllSettled— typed tuple-preserving wrappers.pMap(items, mapper, { concurrency, stopOnError }).pSeries,pFilter.defer<T>()— externally-resolvable promise.debounceAsync(fn, wait).
Random
Random.pick,Random.sample,Random.weighted.Random.uuid()(usescrypto.randomUUIDwhen available).Random.nanoid(size),Random.token(bytes)(crypto-backed where possible).Random.float(min, max, precision?).Random.seed(seed)— mulberry32 reproducible mode.
Lazy (existing API kept, additions only)
lazy.async(producer)— async lazy reference.lazy.from(value)— pre-resolved lazy.type LazyAsync<T>.
Mixed
coalesce(...values)— first non-nullish.