I have a scenario where I need to grab the first occurrence of a string from an object, but only if the match occurs in one of the path's that have been pre-defined.

{ id: 'I60ODI', description: 'some random description' }
{ foo: 'bar', description: { color: 'green', text: 'some description within text' } }

When provided either of the two objects above, I would expect the solution to return either some random description or some description within text, provided that the two possible paths are obj.description and obj.description.text. New paths might also need to be added in the future, so it needs to be easy to add them.

Here is the solution I have implemented so far, but to me it doesn't seem optimal.

// require the ramda library
const R = require('ramda');

// is the provided value a string?
const isString = R.ifElse(R.compose(R.equals('string'), (val) => typeof val), R.identity, R.always(false));
const addStringCheck = t => R.compose(isString, t);

// the possible paths to take (subject to scale)
const possiblePaths = [
    R.path(['description', 'text'])
// add the string check to each of the potential paths
const mappedPaths = R.map((x) => addStringCheck(x), possiblePaths);

// select the first occurrence of a string 
const extractString = R.either(...mappedPaths);

// two test objects
const firstObject = { description: 'some random description' };
const secondObject = { description: { text: 'some description within text' } };
const thirdObject = { foo: 'bar' };

console.log(extractString(firstObject)); // 'some random description'
console.log(extractString(secondObject)); // 'some description within text'
console.log(extractString(thirdObject)); // false

I would really appreciate it if a seasoned functional programmer might provide me with some alternative approaches to implementation. Thanks.


A few points:

  • your isString function isn't exactly checking if it's a string, as it's also returning false if not. The typeof check can be simplified to R.is(String)
  • It's probably better to push all the inputs to the edge of the logic, in this case making paths an argument too. Just splits out our logic an our inputs a bit better.
  • Instead of wrapping each path in R.path, you can make a function which turns all paths into path functions
  • For applying the list of path functions to the object, we can use R.juxt

Possible refactoring:

const isString = R.is(String)
const makePaths = R.map(R.path)

const extractStringFor = (paths) =>

const firstObject = { description: 'some random description' };
const secondObject = { description: { text: 'some description within text' } };
const thirdObject = { foo: 'bar' };

const paths = [['description'], ['description', 'text']]
const extractString = extractStringFor(paths)

extractString(firstObject), // 'some random description'
extractString(secondObject), // 'some description within text'
extractString(thirdObject) // false

caveat: This runs all paths against each object, and then find the match that's a string. That could be optimised though, potentially with R.takeWhile/R.reduceWhile or something.

  • \$\begingroup\$ I like this version, although I added my own two cents in another answer. One suggestion I would have, though: unless you're reusing them, isString and makePaths are simple enough to include inline in the main function; it's just as readable when you include them directly. \$\endgroup\$ – Scott Sauyet Oct 7 '17 at 22:12

(Answer moved from https://stackoverflow.com/questions/46619331. I thought it was worth moving because it has several interesting points, although I think the answer from @RossMackay is overall better.)

This will work, and I think it's cleaner:

const extract = curry((defaultVal, paths, obj) => pipe( 
  find(pipe(path(__, obj), is(String))),
  ifElse(is(Array), path(__, obj), always(defaultVal))

const paths = [['description'], ['description', 'text']]

extract(false, paths, firstObject)  //=> "some random description"
extract(false, paths, secondObject) //=> "some description within text"
extract(false, paths, thirdObject)  //=> false

I personally would find a better default in '' than in false, but that's your call.

This avoids mapping over all the paths, stopping when the first one is found. It also uses Ramda's is to replace your complex isString with R.is(String). And the currying lets you supply the first or the first two parameters to create a more useful function.

You can see this in action in the Ramda REPL.


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.