Skip to content

Releases: radashi-org/radashi

v12.9.0

05 May 20:23

Choose a tag to compare

New Functions

Add mergeOptions function → PR #442

Merge two optional option objects with runtime behavior and TypeScript inference that both understand undefined. mergeOptions shallow-merges two defined objects, lets the second object override overlapping keys, and returns the defined argument when either side is undefined.

  • Models T | undefined inputs without Object.assign workarounds
  • Preserves optional properties and required override keys in the result type
  • Returns undefined when both inputs are undefined
import * as _ from 'radashi'

const defaults = { port: 3000, host: 'localhost' }
const userConfig = { port: 8080, ssl: true }

_.mergeOptions(defaults, userConfig)
// => { port: 8080, host: 'localhost', ssl: true }

_.mergeOptions(undefined, userConfig)
// => { port: 8080, ssl: true }

_.mergeOptions(undefined, undefined)
// => undefined

Docs / Source / Tests / Type Tests

New Features

Improve sort return type when sorting a tuple → PR #421

Sorting a tuple now preserves its tuple length in the return type instead of widening the result to a plain array. This keeps tuple-aware inference intact for as const inputs while preserving the same runtime behavior: sort still copies the input and returns a mutable sorted array.

  • Regular arrays still return mutable arrays
  • Readonly arrays still return mutable arrays
  • Const tuples now return a mutable tuple with the same length
import * as _ from 'radashi'

const list = [{ index: 2 }, { index: 0 }, { index: 1 }] as const

const result = _.sort(list, item => item.index)
// type: [(typeof list)[number], (typeof list)[number], (typeof list)[number]]
// => [{ index: 0 }, { index: 1 }, { index: 2 }]

Docs / Source / Type Tests

v12.8.1

05 May 17:59

Choose a tag to compare

Fixed

  • Mark dedent as side-effect free in 7ada48f

v12.8.0

05 May 15:04

Choose a tag to compare

New Functions

Add deburr function → PR #449

Normalize accented Latin text into plain ASCII-friendly strings. deburr strips combining marks and expands common extended Latin letters and ligatures, which is useful before slugging, searching, or comparing user-facing text.

  • Removes Unicode combining marks after NFD normalization
  • Handles common Latin characters that do not decompose cleanly, like Æ, ø, ß, and Ł
  • Leaves other characters untouched, so it stays focused on transliterating Latin text
import * as _ from 'radashi'

_.deburr('Crème Brûlée') // => 'Creme Brulee'
_.deburr('Ærøskøbing') // => 'Aeroskobing'
_.deburr('Straße') // => 'Strasse'

Docs / Source / Tests

Add getErrorMessage function → PR #466

Turn unknown caught values into a readable message that is safe to display or log. getErrorMessage returns messages from Error instances and non-empty strings, then falls back to "Unknown error." for empty or unsupported values.

  • Works with Error subclasses such as TypeError
  • Accepts string throws without wrapping them first
  • Keeps arbitrary objects from being treated as errors just because they have a message property
import * as _ from 'radashi'

try {
  throw new Error('Request failed')
} catch (error) {
  _.getErrorMessage(error) // => 'Request failed'
}

_.getErrorMessage('Request failed') // => 'Request failed'
_.getErrorMessage(null) // => 'Unknown error.'

Docs / Source / Tests

v12.7.2

24 Feb 20:32

Choose a tag to compare

Fixed

  • Chain type signatures after 3 arguments in 17aef87

v12.7.1

19 Nov 07:08

Choose a tag to compare

Fixed

  • Ensure DurationParser static properties can be tree-shaked in b9d7529

v12.7.0

17 Oct 21:58

Choose a tag to compare

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 Map and WeakMap instances
  • 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) // => 10

Inspired 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.is so NaN matches itself while +0 and -0 stay 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]) // => true

🔗 Docs / Source / Tests

Add 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.

  • absoluteJitter offsets the base value by up to ±offset
  • proportionalJitter scales 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!

🔗 Docs / Source / Tests

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 _.identity when 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!

🔗 Docs / Source / Tests

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 objectify call 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!

🔗 Docs / Source / Tests

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 null out 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 empty

Thanks to Nano Miratus for tightening up the typings!

🔗 Source: max.ts · min.ts

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...

Read more

v12.6.2

20 Aug 19:13

Choose a tag to compare

Fixed

  • (range) Ensure end parameter works when 0 in 9c8ffa0

v12.6.1

09 Aug 14:11

Choose a tag to compare

Fixed

  • (group) Use Object.create(null) for the returned object in 5db8c37

v12.6.0

26 Jun 19:56

Choose a tag to compare

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 asserts keyword.
  • Accepts an optional message (string or Error instance) for failed assertions.
  • assert(false, ...) has a never return type for unreachable code paths.
  • Inspired by Node.js's assert module.
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
// - false

🔗 Docs / Source / Tests

Add 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, &quot;5 &lt; 10 &amp; that&#39;s obvious.&quot;'

🔗 Docs / Source / Tests

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 DurationParser class 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_000

Thanks to Alec Larson and @hugo082 for their work on this feature!

🔗 Docs / Source / Tests

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 QuantityParser class 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)
// => 500

Thanks to Alec Larson and @hugo082 for their work on this feature!

🔗 Docs / Source / Tests

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!

🔗 Docs / Source / Tests

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' })

🔗 Docs / Source / Tests

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 and release() 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!

🔗 Docs / Source / Tests

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

🔗 Docs / Source / Tests

Fixes

v12.5.1

23 May 21:41

Choose a tag to compare

Fixed