Skip to Content

Day 85: on nesting

Posted on 5 mins read

You know when you’re writing the best Javascript of your life, and then you suddenly you make one api call and you get something that looks like this:

{
  result: {
    search: {
      thisThing: {
        thatThing: {
          theThingReallyWanted: {
            theActualValueWanted: "Hey, hi, hello"
          }
        }
      }
    }
  } 
}

And you realize sometimes, thisThing might not exist. And then other times, thatThing might not exist. And, you know what, why not even say that sometimes not even result might exist? Suddenly, your very nice Javascript has a line that looks like:

if(result && result.search && result.search.thisThing ... && result.search.thisThing.thatThing.theThingReallyWanted ) return result.search.thisThing.thatThing.theThingReallyWanted.theActualValueWanted

Well, that happens to everyone in Javascript and it’s one of my personal pet peeves with the language. I hate nested properties and also hate the fact that accessing nested properties is either an act of bravery or of repetition. So, I wrote a function. Actually, 3 functions that do something like that. Kind of. Here goes.

Possibility

In FP languages, there’s this structure called Maybe or Option which basically deals with this sort of thing. I talked about it here, and it provides a safe way to access things that may not exist without fear of blowing up 💥. Buuut, as an exercise in trying out different approaches, I didn’t want to use the Maybe data structure this time, although I will borrow a bit from its concept.

So, since we like to write tests first, here are the first few:

describe("The safeProperty function", () => {
  it("should return NOTHING if the property doesn't exist", () => {
    const expected = NOTHING;
    const actual = safeProperty({}, "prop");
    expect(expected).toBe(actual);
  });

  it("should return the property if it exists", () => {
    const expected = "property";
    const actual = safeProperty({ prop: "property" }, "prop");
    expect(expected).toBe(actual);
  });
});
Note: in real life, we’d want to write one test at a time, but this isn’t a TDD article, it’s a JS frustration and discovery article 🦄

So, we assume that there’s something called NOTHING that represents, well, nothing, or the absence of existence. Whatever that is, depends on you though. And, we say that if the property doesn’t exist, we return nothing, else we return the value of the property.

Now, here’s the function that does that:

const safeProperty = (obj, propertyName) => (obj[propertyName] ? obj[propertyName] : NOTHING);

If exists, that, else NOTHING. This is a good start, but we’re still missing something. It doesn’t look for nested properties. So, here come more tests:

describe("The recurProperties function", () => {
  it("should return NOTHING if the first property doesn't exist", () => {
    const expected = NOTHING;
    const actual = recurProperties({}, "prop");
    expect(expected).toBe(actual);
  });

  it("should return NOTHING if the second property doesn't exist", () => {
    const expected = NOTHING;
    const actual = recurProperties({ prop: "notAnObject" }, "prop", "secondProp");
    expect(expected).toBe(actual);
  });

  it("should return return the second property if it exists", () => {
    const expected = "hello";
    const actual = recurProperties({ prop: { secondProp: "hello" } }, "prop", "secondProp");
    expect(expected).toBe(actual);
  });
});

This time, we want to test the same behaviour, but over different levels of nesting inside an object. And, here’s how we can do that:

const recurProperties = (obj, ...propertyNames) =>
  propertyNames.reduce(
    (accValue, currentProperty) =>
      accValue[currentProperty] ? accValue[currentProperty] : NOTHING,
    obj
  );

Let’s unpack that a bit, line by line:

  • const recurProperties = (obj, ...propertyNames) => function definition. It takes an object, and then any number of property names (as strings). We use the spread operator so that all parameters that come after the first one go into an array called propertyNames.
  • propertyNames.reduce( (accValue, currentProperty) => accValue[currentProperty] ? accValue[currentProperty] : NOTHING, obj); This is the meaty part. First, we reduce over the propertyNames array, and for each one, check if the property exists in the current object. If it does, we return the value of that property, else we return NOTHING to the next iteration. Since NOTHING will (hopefully) never contain a property by any name, then we can assume that the function will keep returning NOTHING. Otherwise, it will recursively examine the existence of nested properties, until the propertyNames array runs out of elements.

This works. It passes its unit tests, which granted, aren’t that many, but it works. So, now we could use it like this:

const whatIreallyWant = recurProperties(apiResult, result, search, thisThing, thatThing, theThingReallyWanted, theActualValueWanted)

Long, but still better than the other option, right? And you can put all the strings into an array and spread it inside the function call and do all sorts of fun things here. So, this is perfect, if we only need to grab the value. But what if we want to do something with it? Well ⬇

describe("The ifPropertyThen function", () => {
  it("should return Nothing if the property doesn't exist", () => {
    const expected = NOTHING;
    const actual = ifPropertyThen(x => x, {}, "prop");
    expect(expected).toBe(actual);
  });

  it("should apply the function given to the object passed and return its result", () => {
    const expected = { prop: 2 };
    const actual = ifPropertyThen(
      x => {
        x.prop++;
        return x;
      },
      { prop: 1 },
      "prop"
    );
    expect(expected).toEqual(actual);
  });
});

And, the function that passes those tests is:

const ifPropertyThen = (fn, obj, ...propertyNames) =>
  recurProperties(obj, ...propertyNames) === NOTHING ? NOTHING : fn(obj);

It gets a function that will get applied to the object in case the property exists, otherwise it’ll just return NOTHING. Again.

Patterns

I set out to try and solve this without Maybe and it worked out fine. A bit ugly, but fine. There’s a clear pattern of repetition there though, which is always passing NOTHING along whenever any of the steps blows up. And guess what, that’s something that Maybe does quite well.

In the end, a better version of this code would be to just have an implementation of Maybe and use it in all the functions, coming back to the fact that learning Haskell proves useful beyond itself 🎉🎊.

Here’s a Gist to play around with that code if you like.

comments powered by Disqus