javascriptfunctional-programmingramda.jssanctuary

What's the cleanest fp way to get a property pointed by another property


Given an object that may be null and may have the following properties:

{
  templateId: "template1",
  templates: {
    template1: "hello"
  }
}

How would you get the template in a failsafe way? (templateId might not be defined, or the template it reffers might be undefined)

I use ramda and was trying to adapt my naive version of the code to use something like a Maybe adt to avoid explicit null/undefined checks.

I'm failing to come up with an elegant and clean solution.

naive ramda version:

const getTemplate = obj => {
  const templateId = obj && prop("templateId", obj);
  const template = templateId != null && path(["template", templateId], obj);
  return template;
}

this does work but I would like to avoid null checks, as my code has a lot more going on and it would be really nice to become cleaner

Edit I get from severall answers that the best is to ensure clean data first. That's not allways possible though. I also came up with this, which I do like.

const Empty=Symbol("Empty"); 
const p = R.propOr(Empty);
const getTemplate = R.converge(p,[p("templateId"), p("templates")]);

Would like to get feedback regarding how clean and how readable it is (and if there are edge cases that would wreck it)


Solution

  • As others have told you, ugly data precludes beautiful code. Clean up your nulls or represent them as option types.

    That said, ES6 does allow you to handle this with some heavy destructuring assignment

    const EmptyTemplate =
      Symbol ()
    
    const getTemplate = ({ templateId, templates: { [templateId]: x = EmptyTemplate } }) =>
      x
      
    console.log
      ( getTemplate ({ templateId: "a", templates: { a: "hello" }}) // "hello"
      , getTemplate ({ templateId: "b", templates: { a: "hello" }}) // EmptyTemplate
      , getTemplate ({                  templates: { a: "hello" }}) // EmptyTemplate
      )

    You can continue to make getTemplate even more defensive. For example, below we accept calling our function with an empty object, and even no input at all

    const EmptyTemplate =
      Symbol ()
    
    const getTemplate =
      ( { templateId
        , templates: { [templateId]: x = EmptyTemplate } = {}
        }
      = {}
      ) =>
        x
     
    console.log
      ( getTemplate ({ templateId: "a", templates: { a: "hello" }}) // "hello"
      , getTemplate ({ templateId: "b", templates: { a: "hello" }}) // EmptyTemplate
      , getTemplate ({                  templates: { a: "hello" }}) // EmptyTemplate
      , getTemplate ({})                                            // EmptyTemplate
      , getTemplate ()                                              // EmptyTemplate
      )

    Above, we start to experience a little pain. This signal is important not to ignore as it warns us we're doing something wrong. If you have to support that many null checks, it indicates you need to tighten down the code in other areas of your program. It'd be unwise to copy/paste any one of these answers verbatim and miss the lesson everyone is trying to teach you.