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);
};
}
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);
});
Quick sketch with State monad