Skip to content

Instantly share code, notes, and snippets.

@JamieMason
Last active February 8, 2023 10:54
Show Gist options
  • 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);
});
@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