0

I'm new to functional programming but wanna learn best practices.

What is the proper fp-ts way to convert array into object?

(items: Item[], keyGetter: (i: Item) => Key) => Record<Key, Item>

I use my own not fp-ts implementation so far:

function isDefined<T>(value: T): value is Exclude<T, undefined> {
  return value !== undefined;
}

type TIdentifier = string | number;

export const arrayToRecord = <T1, T2 extends TIdentifier = string>(
  arr: T1[],
  getKeyName?: (item: T1) => T2
): Record<T2, T1> => {
  const hasKeyNameGetter = isDefined(getKeyName);
  return arr.reduce((acc, item) => {
    acc[
      hasKeyNameGetter ? (getKeyName as (item: T1) => T2)(item) : ((item as unknown) as T2)
    ] = item;
    return acc;
  }, {} as Record<T2, T1>);
};

3 Answers3

4

This is the only fp-ts solution:

import * as A from "fp-ts/lib/Array";
import * as R from "fp-ts/lib/Record";
import * as semigroup from 'fp-ts/Semigroup';

const arr = A.fromArray([1,2,3]);

const testRecord = R.fromFoldableMap(
  semigroup.last<number>(),
  A.array
)(arr, key => [String(key), key]);
1

Here's a way to achieve what you're asking.

Some notes:

  • since the dictionary is built at runtime and there is no guarantee on the keys, to prevent unsafe code the return type is Record<string, A>
  • keyGetter can't be optional, we must provide a way to came up with e key
import * as A from 'fp-ts/ReadonlyArray'
import * as R from 'fp-ts/ReadonlyRecord'
import { pipe } from 'fp-ts/function'

const arrayToRecord = <A>(
  items: ReadonlyArray<A>,
  keyGetter: (i: A) => string,
): Readonly<Record<string, A>> =>
  pipe(
    items,
    A.reduce({}, (acc, item) => pipe(acc, R.upsertAt(keyGetter(item), item))),
  )

EDIT

An example as requested:

const xs = [
  { id: 'abc', date: new Date() },
  { id: 'snt', date: new Date() },
]
const res = arrayToRecord(xs, (x) => x.id)

console.log(res)
// {
//   abc: { id: 'abc', date: 2021-04-06T13:09:25.732Z },
//   snt: { id: 'snt', date: 2021-04-06T13:09:25.732Z }
// }

EDIT 2

pipe friendly version:

declare const arrayToRecord: <A>(
  keyGetter: (i: A) => string,
) => (items: ReadonlyArray<A>) => Readonly<Record<string, A>>

interface X { id: string; date: Date }

declare const xs: ReadonlyArray<X>

pipe(
  xs,
  arrayToRecord((x) => x.id),
) // Readonly<Record<string, X>>
Denis Frezzato
  • 957
  • 6
  • 15
  • Thanks! Could you pls extend your answer with use-case, like put this function in work. Let's say we have array of items and keyGetter fn, then how to put in in pipe fp-ts way? – Dmitrii Bykov Apr 06 '21 at 12:59
  • @DmitriiBykov I've added it. – Denis Frezzato Apr 06 '21 at 13:11
  • I can see your editing but I meant another thing. How to use it inside pipe and other thing from fp-ts? – Dmitrii Bykov Apr 06 '21 at 16:07
  • If you want that function to be "pipe friendly" you have to change its type signature to `(keyGetter: (i: A) => string) => (items: ReadonlyArray) => Readonly>` usage: `pipe(xs, arrayToRecord((x) => x.id))` – Denis Frezzato Apr 06 '21 at 16:14
  • Is there a way to make this pipe flat, kinda `pipe(xs, (x) => x.id, arrayToRecord)`. Mb some special pf-ts data structure? – Dmitrii Bykov Apr 07 '21 at 02:51
  • No, `pipe` it's not a magic box, it just composes functions, so `pipe(x, f, g)` is equivalent to `g(f(x))`. `pipe(xs, (x) => x.id, arrayToRecord)` that you wrote is equivalent to `arrayToRecord(((x) => x.id)(xs))`, which is not what you want. – Denis Frezzato Apr 07 '21 at 06:53
  • Yeah, I know. The purpose of my question is to find the proper way to rewrite and apply arrayToRecord in fp-ts pipe using it's native structures. Rewriting arrayToRecord using fp-ts under the hood it's enough. I want to learn elegant fp-ts way to use arrayToRecord inside the pipe. Perhaps some special fp-ts data structures can help to make `pipe(xs, keyGetter, arrayToRecord)` work. keyGetter and arrayToRecord can return Ether or smth else – Dmitrii Bykov Apr 07 '21 at 09:37
  • 2
    I don't see any way to obtain this `pipe(xs, keyGetter, arrayToRecord)` (and I actually don't see why you want to do this exercise). Furthermore, `fp-ts` is full of higher-order functions, so you usually end up with this, for example: `pipe(O.some(1), O.map(n => n + 1), O.chain((n => n % 2 == 0 ? O.some(n) : O.none))`. – Denis Frezzato Apr 07 '21 at 10:35
0

Here's a couple of solutions that build on Dmitrii's idea to use fromFoldableMap and make it generic as in Denis' answer:

const arrayToRecord =
  <T>(keyGetter: (i: T) => string) =>
  (items: ReadonlyArray<T>): Readonly<Record<string, T>> =>
    R.fromFoldableMap(last<T>(), A.Foldable)(items, (item) => [keyGetter(item), item]);

// functionally equivalent, maybe more readable
const arrayToRecord2 = <T>(keyGetter: (i: T) => string) =>
  flow(
    A.map<T, readonly [string, T]>((item) => [keyGetter(item), item]),
    R.fromFoldable(last<T>(), A.Foldable)
  );

Here's an example (yes, it is exactly the same as Denis' example):

  const xs = [
    { id: "abc", date: new Date() },
    { id: "snt", date: new Date() },
  ];
  const res = pipe(xs, arrayToRecord((x) => x.id));
  console.log(res);
// {
//   abc: { id: 'abc', date: 2021-04-06T13:09:25.732Z },
//   snt: { id: 'snt', date: 2021-04-06T13:09:25.732Z }
// }

  const res2 = pipe(xs, arrayToRecord2((x) => x.id));
  console.log(res2);
// {
//   abc: { id: 'abc', date: 2021-04-06T13:09:25.732Z },
//   snt: { id: 'snt', date: 2021-04-06T13:09:25.732Z }
// }