Dmitrii Bykov
Dmitrii Bykov

Reputation: 55

The proper fp-ts way to convert array into object

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

Upvotes: 0

Views: 2641

Answers (3)

filippopanelli
filippopanelli

Reputation: 41

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 }
// }

Upvotes: 0

Dmitrii Bykov
Dmitrii Bykov

Reputation: 55

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]);

Upvotes: 4

Denis Frezzato
Denis Frezzato

Reputation: 968

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

Upvotes: 1

Related Questions