npm package: @jcoreio/utils
Description: lodash/es-toolkit-style functions redesigned to support Iterables, Maps, and type-safe functional programming
Author: Andy Edwards
Latest version: 0.0.0-development

# @jcoreio/utils

`lodash`/`es-toolkit` style functions redesigned to support `Iterable`s, `Map`s, and type-safe functional programming

[![CircleCI](https://circleci.com/gh/jcoreio/utils.svg?style=svg)](https://circleci.com/gh/jcoreio/utils)
[![Coverage Status](https://codecov.io/gh/jcoreio/utils/branch/master/graph/badge.svg)](https://codecov.io/gh/jcoreio/utils)
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
[![npm version](https://badge.fury.io/js/%40jcoreio%2Futils.svg)](https://badge.fury.io/js/%40jcoreio%2Futils)

# Overview

`@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:

### Functions that accept arrays like `filter` also accept `Iterable`s

```ts
import { filter } from '@jcoreio/utils/filter'
console.log(filter(new Set([1, 2, 3]), (x) => x > 1)) // [2, 3]
```

### Functions that accept objects like `mapValues` also accept `Map`s and `Iterable`s of key/value pairs

```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]]
```

### Stronger type safety guarantees

`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 `<function>.unsafe` 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
}
```

### Functions can be called in either normal or functional programming style

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
`pipe`lines.

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<Tags extends PropertyKey = string> = { [Tag in Tags]?: TagData }

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