Releases: radashi-org/radashi
v12.7.1
v12.7.0
New Functions
Add getOrInsert and getOrInsertComputed functions → PR #444
Access or initialize map entries without boilerplate branching. getOrInsert writes the provided value once, while getOrInsertComputed lazily creates an entry only when it is missing.
- Works with both
MapandWeakMapinstances - Returns the stored entry so you can chain additional logic
- Avoids unnecessary factory calls when a key already exists
import * as _ from 'radashi'
const counts = new Map<string, number>()
_.getOrInsert(counts, 'clicks', 1) // => 1
_.getOrInsert(counts, 'clicks', 5) // => 1
_.getOrInsertComputed(counts, 'views', () => 10) // => 10
_.getOrInsertComputed(counts, 'views', () => 0) // => 10Inspired by TC39's upsert proposal.
🔗 Docs: getOrInsert · getOrInsertComputed / Source: getOrInsert.ts · getOrInsertComputed.ts / Tests: getOrInsert.test.ts · getOrInsertComputed.test.ts
Add isArrayEqual function → PR #417
Compare arrays with Object.is precision. isArrayEqual checks length and element identity, correctly handling tricky cases like NaN, sparse arrays, and the +0/-0 distinction.
- Uses
Object.issoNaNmatches itself while+0and-0stay distinct - Short-circuits when lengths differ for a fast inequality check
- Leaves the original arrays untouched
import * as _ from 'radashi'
_.isArrayEqual([1, 2, 3], [1, 2, 3]) // => true
_.isArrayEqual([0], [-0]) // => false
_.isArrayEqual([Number.NaN], [Number.NaN]) // => trueAdd isMapEqual and isSetEqual functions → PR #437
Quickly compare collections without writing loops. isMapEqual uses isEqual to traverse nested values, while isSetEqual focuses on membership equality for reference types.
- Checks map sizes first, then verifies each key/value pair deeply
- Compares set entries via
Set#has, perfect for shared object references or primitive values - Gives you targeted equality helpers instead of overloading
isEqual
import * as _ from 'radashi'
const left = new Map([
['id', 1],
['tags', ['radashi', 'bench']],
])
const right = new Map([
['tags', ['radashi', 'bench']],
['id', 1],
])
_.isMapEqual(left, right) // => true
const user = { id: 1 }
_.isSetEqual(new Set([user]), new Set([user])) // => true🔗 Docs: isMapEqual · isSetEqual / Source: isMapEqual.ts · isSetEqual.ts / Tests: isMapEqual.test.ts · isSetEqual.test.ts
Add absoluteJitter and proportionalJitter functions → PR #446
Inject randomized noise into numbers for simulations, experiments, or simple variability. Choose an absolute range or a proportional factor depending on the use case.
absoluteJitteroffsets the base value by up to ±offsetproportionalJitterscales jitter by a percentage of the base value- Designed for symmetric distributions using
Math.random()under the hood
import * as _ from 'radashi'
const base = 100
_.absoluteJitter(base, 5) // => between 95 and 105
_.proportionalJitter(base, 0.1) // => between 90 and 110🔗 Docs: absoluteJitter · proportionalJitter / Source: absoluteJitter.ts · proportionalJitter.ts / Tests: absoluteJitter.test.ts · proportionalJitter.test.ts
Add identity function → PR #422
The identity helper simply returns the value you pass in, providing a lightweight default callback for APIs that expect a mapper function.
- Fully generic, so TypeScript infers the original value type
- Handy as a default getter when working with utilities like
sort - Works even when no argument is supplied, returning
undefined
import * as _ from 'radashi'
_.identity() // => undefined
_.identity('radashi') // => 'radashi'
_.identity({ id: 1 }) // => { id: 1 }Thanks to Nano Miratus for adding this functional building block!
New Features
Use identity as the default getter for sort → PR #423
sort now handles raw numeric arrays without a custom getter. When you omit the getter, it falls back to identity, keeping the API ergonomic while preserving the ability to switch to descending order.
- Explicitly pass
_.identitywhen you want to sort descending - Still clones the array, leaving your original list untouched
import * as _ from 'radashi'
const numbers = [2, 0, 1]
_.sort(numbers) // => [0, 1, 2]
_.sort(numbers, _.identity, true) // => [2, 1, 0]Thanks to Nano Miratus for smoothing out this API!
Allow objectify callbacks to read the item index → PR #440
Both getKey and getValue callbacks now receive the item index, making it easy to build composite keys or inject positional data while converting arrays into dictionaries.
- Keep keys unique by appending the index to collisions
- Shape return values with both the item and its position
- Works seamlessly with existing
objectifycall sites
import * as _ from 'radashi'
const list = [
{ id: 'a', word: 'hello' },
{ id: 'b', word: 'bye' },
]
_.objectify(
list,
(item, i) => `${item.id}_${i}`,
(item, i) => `${item.word}-${i}`,
)
// => { a_0: 'hello-0', b_1: 'bye-1' }Thanks to Ronen Barzel for extending objectify!
Preserve tuple types when using min and max getters → PR #436
When you pass a getter to min or max, the helper now returns the original tuple element instead of widening to T | null. That keeps discriminated unions and as const tuples fully typed.
- New overloads ensure non-empty tuples come back as the same literal type
- Keeps
nullout of the result when the tuple has at least one item - Helps TypeScript infer richer shapes in downstream code
import * as _ from 'radashi'
const sizes = [
{ label: 'S', weight: 8 },
{ label: 'XL', weight: 12 },
] as const
const biggest = _.max(sizes, size => size.weight)
// biggest is non-nullable, since sizes is known to never be emptyThanks to Nano Miratus for tightening up the typings!
Documentation
Clarify that unique preserves original ordering → PR #433
The docs now state that unique keeps the first occurrence of each item. Examples and tests were updated to highlight the stable ordering and refreshed copy for clarity.
- Explicitly documents that duplicates keep their earliest entry
- Updates the example data to match the behavior
- Adds a unit test covering order preservation
import * as _ from 'radashi'
const fish = [
{ name: 'Trout', source: 'lake' },
{ name: 'Salmon', source: 'stream' },
{ name: 'Salmon', source: 'river' },
]
_.unique(fish, item => item.name)
// => [Trout, Salmon]Thanks to Ronen Barzel for polishing the documentation!
🔗 Docs / [Tests](https://github.com/radashi-org/r...
v12.6.2
v12.6.1
v12.6.0
New Functions
Add assert function → PR #403
The assert function from Radashi is used to assert that a given condition is true. If the condition evaluates to false, the function throws an error. This is a fundamental building block for ensuring that certain conditions are met at runtime. This utility is particularly useful in TypeScript for its ability to perform type narrowing.
- Asserts a condition and throws an error if false.
- Useful for TypeScript type narrowing using the
assertskeyword. - Accepts an optional message (string or Error instance) for failed assertions.
assert(false, ...)has aneverreturn type for unreachable code paths.- Inspired by Node.js's
assertmodule.
import * as _ from 'radashi'
function processValue(value: string | null | undefined) {
_.assert(value, 'Value cannot be null, undefined, or empty')
// After the assertion, 'value' is narrowed to type 'string'
console.log(value.toUpperCase())
}
processValue('hello') // logs "HELLO"
// _.assert throws on falsy values like:
// - null
// - undefined
// - '' (empty string)
// - 0
// - falseAdd escapeHTML function → PR #401
Replaces all occurrences of specific characters with their corresponding HTML entities to escape HTML in a string.
&is replaced with&<is replaced with<>is replaced with>"is replaced with"'is replaced with'
import * as _ from 'radashi'
_.escapeHTML(`Sarah said, "5 < 10 & that's obvious."`)
// => 'Sarah said, "5 < 10 & that's obvious."'Add parseDuration function → PR #416
Parses a human-readable duration string (like "1 hour", "2 seconds") into milliseconds.
- Supports units like millisecond, second, minute, hour, day, and week.
- Custom units can be added.
- A
DurationParserclass is available for more efficient repeated parsing.
import * as _ from 'radashi'
_.parseDuration('1 second') // => 1_000
_.parseDuration('1h') // => 3_600_000
_.parseDuration('1 hour') // => 3_600_000
_.parseDuration('1.5 hours') // => 5_400_000
_.parseDuration('-1h') // => -3_600_000Thanks to Alec Larson and @hugo082 for their work on this feature!
Add parseQuantity function → PR #416
Parses a quantity string like "2 dollars" into its numeric value. You must provide a unit conversion map, with optional short unit aliases.
- Requires a unit conversion map.
- Supports optional short unit aliases.
- A
QuantityParserclass is available for more efficient repeated parsing and subclassing.
import * as _ from 'radashi'
const moneyUnits = {
units: {
cent: 1,
dollar: 100,
},
short: {
$: 'dollar',
},
} as const
_.parseQuantity('1 cent', moneyUnits)
// => 1
_.parseQuantity('2 dollars', moneyUnits)
// => 200
_.parseQuantity('5$', moneyUnits)
// => 500Thanks to Alec Larson and @hugo082 for their work on this feature!
Add promiseChain function → PR #402
Chain together multiple, potentially asynchronous functions. The result of each function is passed to the next function.
- Executes functions in the order they are provided.
- Supports both synchronous and asynchronous functions.
- Returns a Promise with the final result.
import * as _ from 'radashi'
const func1 = (a, b) => a + b
const func2 = async n => n * 2
const func3 = async n => `Your Value is ${n}`
const chained = _.promiseChain(func1, func2, func3)
await chained(5, 2) // => "Your Value is 14"Thanks to Bharat Soni for their work on this feature!
Add queueByKey function → PR #407
Wraps an asynchronous function to ensure that calls with the same key are queued and executed sequentially, while calls with different keys can run in parallel. This is useful for preventing race conditions when operations must not overlap for the same logical group (like user ID or resource ID).
- Sequential per key: Operations with the same key execute one after another
- Parallel across keys: Operations with different keys run concurrently
- Error handling: Errors are properly propagated and don't break the queue
- Memory efficient: Queues are automatically cleaned up when empty
- Type safe: Full TypeScript support with generic types
import * as _ from 'radashi'
const updateUser = async (userId: string, data: object) => {
// Simulate API call that shouldn't overlap for the same user
const response = await fetch(`/api/users/${userId}`, {
method: 'POST',
body: JSON.stringify(data),
})
return response.json()
}
const queuedUpdate = _.queueByKey(updateUser, userId => userId)
// These will run sequentially for user123
queuedUpdate('user123', { name: 'Alice' })
queuedUpdate('user123', { age: 30 })
// This runs in parallel with user123's queue
queuedUpdate('user456', { name: 'Bob' })Add Semaphore class → PR #415
A synchronization primitive that allows a limited number of concurrent operations to proceed.
- Limits the number of concurrent operations.
- Use
acquire()to get a permit andrelease()to free it. - Supports acquiring permits with a specific weight.
- Pending acquisitions can be aborted using
AbortController. - All pending and future acquisitions can be rejected using
semaphore.reject().
import { Semaphore } from 'radashi'
const semaphore = new Semaphore(2)
const permit = await semaphore.acquire()
permit.release()Thanks to Alec Larson and @hugo082 for their work on this feature!
New Features
Pass array index to group callback → Commit 6d66395
The callback function provided to the group function now receives the array index as its second argument. This allows for more flexible grouping logic that can take into account the position of elements in the original array.
- The callback signature is now
(item, index) => groupKey. - Enables grouping based on element position as well as value.
import * as _ from 'radashi'
const items = ['a', 'b', 'c', 'd', 'e']
const groupedByIndex = _.group(items, (item, index) =>
index % 2 === 0 ? 'even' : 'odd',
)
// => { even: ['a', 'c', 'e'], odd: ['b', 'd'] }Fixes
- Fix incorrect type narrowing in
isIntString(→ PR #412) by Igor Golovin - Fix filtering out null from
selectFirstreturn type when no condition is given (→ PR #413) by Ali Medhat
v12.5.1
Fixed
- (set) Avoid mutating nested objects without first cloning them by @aleclarson in fc3c7c9
- (set) Prevent prototype pollution by @aleclarson in 8147abc
v12.5.0
New Functions
Add concat function #388
Flattens and filters nullish values from arguments.
import { concat } from 'radashi'
const result = concat('a', null, ['b', undefined], 'c')
// => ['a', 'b', 'c']Add pluck function #376
Map an array of objects to an array of arrays.
import { pluck } from 'radashi'
const gods = [
{ name: 'Ra', power: 100 },
{ name: 'Zeus', power: 98 },
]
const names = pluck(gods, ['name'])
// => [['Ra'], ['Zeus']]Thanks to @nusohiro for the contribution!
Fixed
Fix mapify index argument #384
Ensure the index argument passed to mapify's 2nd callback is the actual array index.
import { mapify } from 'radashi'
const list = [
{ id: 'a', word: 'hello' },
{ id: 'a', word: 'bye' },
{ id: 'a', word: 'oh' },
]
const result = mapify(
list,
x => x.id,
(x, i) => x.word + i,
)
// => Map { 'a' => 'oh2' }Thanks to @Yukiniro for the contribution!
Avoid infinite loop in cluster when size is 0 #397
The cluster function now correctly handles the case where the size is 0 or less by returning an empty array.
import { cluster } from 'radashi'
const result = cluster([1, 2, 3], 0)
// => []Thanks to @fResult for the contribution!
Types
Improve cluster type inference #389
The cluster function now provides precise type inference for common cluster sizes (1-8) using tuple types.
import { cluster } from 'radashi'
const result = cluster(['a', 'b', 'c', 'd'], 2)
// ^? [string, string][]Thanks to @fResult for the contribution!
v12.4.0
New Functions
Add remove function → PR #344
The remove function removes elements from an array based on the specified predicate function.
- Removes elements that satisfy the predicate function.
- Returns a new array with the removed elements.
- Does not mutate the original array.
import * as _ from 'radashi'
const numbers = [1, 2, 3, 4, 5]
const removed = _.remove(numbers, value => value % 2 === 0)
console.log(removed) // Output: [1, 3, 5]Thanks to nusohiro for their work on this feature!
Add toResult function → PR #375
The toResult function converts a PromiseLike to a Promise<Result>.
- Converts a resolved promise to
[undefined, value]. - Converts a rejected promise to
[Error, undefined]. - Rethrows non-Error rejections.
import { toResult, Result } from 'radashi'
const good = async (): Promise<number> => 1
const bad = async (): Promise<number> => {
throw new Error('bad')
}
const goodResult = await toResult(good())
// => [undefined, 1]
const badResult = await toResult(bad())
// => [Error('bad'), undefined]Thanks to Alec Larson for their work on this feature!
Add memoLastCall function → PR #353
The memoLastCall function creates a memoized version of a function that caches only its most recent call. This is useful for optimizing expensive calculations when only the latest result needs to be cached, making it more memory-efficient than traditional memoization.
- Caches the last result of a function call.
- Returns the cached result if the function is called with the same arguments as the previous call.
- Optimizes expensive calculations by avoiding recalculation when the arguments are the same.
import * as _ from 'radashi'
const expensiveCalculation = (x: number, y: number): number => {
console.log('Calculating...')
return x + y
}
const memoizedCalc = _.memoLastCall(expensiveCalculation)
console.log(memoizedCalc(2, 3)) // Outputs: \"Calculating...\" then 5
console.log(memoizedCalc(2, 3)) // Outputs: 5 (uses cached result)
console.log(memoizedCalc(3, 4)) // Outputs: \"Calculating...\" then 7
console.log(memoizedCalc(2, 3)) // Outputs: \"Calculating...\" then 5 (previous cache was overwritten)Thanks to Alec Larson for their work on this feature!
Add isAsyncIterable function → PR #366
The isAsyncIterable function checks if a value is an async iterable.
- Returns
truefor async iterables created by an async generator function. - Returns
truefor objects with a[Symbol.asyncIterator]method. - Returns
falsefor everything else.
import * as _ from 'radashi'
_.isAsyncIterable(
(async function* () {
yield 1
})(),
)
// => true
_.isAsyncIterable([1, 2, 3])
// => falseThanks to Alec Larson for their work on this feature!
Add isBigInt function → PR #369
The isBigInt function returns true if the given value is a BigInt.
- Returns
truewhentypeofreturns'bigint'. - Returns
falsefor everything else.
import * as _ from 'radashi'
_.isBigInt(0n) // => true
_.isBigInt(BigInt(0)) // => true
_.isBigInt(12) // => false
_.isBigInt('0n') // => falseThanks to Shan Shaji for their work on this feature!
New Features
BigInt support in isEmpty → PR #374
The isEmpty function now supports BigInt values.
- Returns
truefor0norBigInt(0).
import * as _ from 'radashi'
_.isEmpty(0n) // => true
_.isEmpty(BigInt(0)) // => true
_.isEmpty(1n) // => falseThanks to Alec Larson for their work on this feature!
v12.3.4
Fixed
- (reduce) Align with native reduce behavior + perf improvements by @aleclarson in #341
v12.3.3
Types
- Let
mapcallback return aPromiseLikeobject by @aleclarson in #330