lodash/es-toolkit-style functions redesigned to support Iterables, Maps, and type-safe functional programming
npm install @jcoreio/utilslodash/es-toolkit style functions redesigned to support Iterables, Maps, and type-safe functional programming




@jcoreio/utils provides many familiar functions like mapValues, groupBy, filter, etc, but with a variety of
important differences from lodash to be more flexible and convenient:
``ts`
import { filter } from '@jcoreio/utils/filter'
console.log(filter(new Set([1, 2, 3]), (x) => x > 1)) // [2, 3]
`ts`
import { mapValues } from '@jcoreio/utils/mapValues'
console.log(
mapValues(
new Map([
['a', 1],
['b', 2],
]),
(x) => x * 2
)
) // Map(2) { 'a' => 2, 'b' => 4 }
console.log(
mapValues(
[
['a', 1],
['b', 2],
],
(x) => x * 2
)
) // [['a', 2], ['b', 4]]
lodash and es-toolkit type defs are unsound when iterating over object values:
`ts
import { mapValues } from 'lodash'
function unsound(x: { a: number }) {
return mapValues(x, (value) => value.toFixed()) // 💥 crashes on b: true
}
const x = { a: 1, b: true }
unsound(x) // 💥 no TS errors, but crashes
`
@jcoreio/utils ensures soundness by default:
`ts
import { mapValues } from '@jcoreio/utils/mapValues'
function sound(x: { a: number }) {
return mapValues(x, (value) => value.toFixed()) // ✘ 'value' is of type 'unknown'. ts(18046)
}
function sound2(x: { [K in string]?: number }) {
// ️in this case, we know 'value' is 'number | undefined' since all string keys have that type
return mapValues(x, (value) => value?.toFixed())
}
`
But you can use to opt into the unsound behavior if you prefer the convenience:
`ts
import { mapValues } from '@jcoreio/utils/mapValues'
function unsound(x: { a: number }) {
return mapValues.unsafe(x, (value) => value.toFixed()) // no TS error
}
`
For example, you can call mapValues in one of two ways:
`ts
// normal style:
mapValues(map, (x) => x * 2)
// functional programming style:
mapValues((x) => x * 2)(map)
`
Functional programming style is mainly intended for ease of use with function pipelines:
`ts
import { mapValues } from '@jcoreio/utils/mapValues'
import { pipe } from '@jcoreio/utils/pipe'
pipe(
map,
mapValues((x) => x * 2),
...
)
`
The overload definitions ensure that type information is fully preserved from function to function in
pipelines.
Here's a real-world example of the convenience of function pipelines for merging chunks of timeseries data by tag:
`ts
import { pipe } from '@jcoreio/utils/pipe'
import { flatMap } from '@jcoreio/utils/flatMap'
import { entries } from '@jcoreio/utils/entries'
import { groupEntries } from '@jcoreio/utils/groupEntries'
import { definedValues } from '@jcoreio/utils/definedValues'
import { mapValues } from '@jcoreio/utils/mapValues'
type TagData = { t: number[]; v: number[]; min?: number[]; max?: number[] }
type Chunk
const chunks: Chunk<'foo' | 'bar'>[] = [
{ foo: { t: [1, 2, 3], v: [4, 5, 6] }, bar: { t: [1], v: [4] } },
{ foo: { t: [4, 5, 6], v: [7, 8, 9], min: [3, 4, 5] } },
]
const merged = pipe(
chunks,
flatMap(entries),
// --> ['foo' | 'bar', TagData | undefined][]
definedValues,
// --> ['foo' | 'bar', TagData][]
groupEntries,
// --> Map<'foo' | 'bar', TagData[]>
mapValues((datas) => ({
t: datas.flatMap((d) => d.t),
v: datas.flatMap((d) => d.v),
}))
// --> Map<'foo' | 'bar', { t: number[], v: number[] }>
)
`
Compare the above the above to the equivalent procedural code:
`ts``
const merged = new Map<'foo' | 'bar', { t: number[]; v: number[] }>()
for (const chunk of chunks) {
for (const [tag, data] of Object.entries(chunk) as [
'foo' | 'bar',
TagData | undefined,
][]) {
if (!data) continue
let group = merged.get(tag)
if (!group) merged.set(tag, (group = { t: [], v: [] }))
for (const t of data.t) group.t.push(t)
for (const v of data.v) group.v.push(v)
}
}