Skip to content

Instantly share code, notes, and snippets.

@JamieMason
Last active February 8, 2023 10:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JamieMason/c0a3b21184cf8c43f76c77878c7c9198 to your computer and use it in GitHub Desktop.
Save JamieMason/c0a3b21184cf8c43f76c77878c7c9198 to your computer and use it in GitHub Desktop.
fp-ts – Lodash's `_.get` and Ramda's `R.props` that returns an `Option`

fp-ts – Lodash _.get, Ramda R.props that returns an Option

A function like Lodash's _.get, Ramda's R.props, and Immutable.js's getIn, written in fp-ts.

import { Json } from 'fp-ts/lib/Json';
import { none, Option, some } from 'fp-ts/lib/Option';

export function getIn<T = Json>(props: string[], origin: unknown): Option<T> {
  let value: unknown = origin;
  for (const prop of props) {
    if (isWalkable(value) && prop in value) {
      value = value[prop];
    } else {
      return none;
    }
  }
  if (value === undefined) return none;
  return some(value as T);
}

function isWalkable(value: unknown): value is Record<string, unknown> {
  return value !== null && ['object', 'function'].includes(typeof value);
}

Version using @mobily/ts-belt:

import { O } from '@mobily/ts-belt';

const isWalkable = (value: unknown): value is Record<string, unknown> =>
  value !== null && typeof value !== 'undefined';

/**
 * Safely read nested properties of any value.
 * @param keys 'child.grandChild.greatGrandChild'
 * @see https://gist.github.com/JamieMason/c0a3b21184cf8c43f76c77878c7c9198
 */
export function props<T>(
  keys: string,
  predicate: (value: unknown) => value is T,
) {
  return function getNestedProp(obj: unknown): O.Option<T> {
    let next = obj;
    for (const key of keys.split('.')) {
      if (isWalkable(next) && key in next) {
        next = next[key];
      } else {
        return O.None;
      }
    }
    return O.fromPredicate(next as any, predicate);
  };
}

Tests

import { O } from '@mobily/ts-belt';
import { isNumber, isString, isUndefined } from 'expect-more';
import { props } from './props';

it('Some when value is found and passes predicate', () => {
  expect(props('a.b', isNumber)({ a: { b: 1 } })).toEqual(O.Some(1));
  expect(props('a.0', isNumber)({ a: [1] })).toEqual(O.Some(1));
});

it('None when value is found but fails predicate', () => {
  expect(props('a.b', isString)({ a: { b: 1 } })).toEqual(O.None);
  expect(props('a.0', isString)({ a: [1] })).toEqual(O.None);
});

it('None when value is not found', () => {
  expect(props('a.b', isString)({})).toEqual(O.None);
  expect(props('a.b', isString)([])).toEqual(O.None);
  expect(props('a.b', isString)(undefined)).toEqual(O.None);
  expect(props('a.b', isString)(null)).toEqual(O.None);
});

it('None when value is not found but matches predicate', () => {
  expect(props('a.b', isUndefined)({})).toEqual(O.None);
  expect(props('a.b', isUndefined)(undefined)).toEqual(O.None);
});
@imcotton
Copy link

Quick sketch with State monad

import {
    state as S,
    option as O,
    function as F,
    readonlyRecord as C,
} from 'fp-ts';

const getIn = (obj: unknown) => F.flow(
    S.traverseArray((k: string) => S.modify(O.chain(C.lookup(k) as never))),
    S.execute(O.fromNullable(obj)),
);

console.log(getIn ({ a: { b: 1 } }) ([ 'a', 'b' ]));

@JamieMason
Copy link
Author

Thanks a lot @imcotton, I'm very new to fp-ts so this is great to learn from.

@imcotton
Copy link

There is one good read about State monad in fp-ts (https://paulgray.net/the-state-monad/).

Also the Lens would be more powerfully and type safety way to go in FP, worth to check out monocle-ts and spectacles-ts on top of that.

@JamieMason
Copy link
Author

Great, thanks for those

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment