loading...
Cover image for JavaScript techniques you wish you knew involving objects

JavaScript techniques you wish you knew involving objects

jackshen profile image Jack Shen ・5 min read

If you've spent any amount of time in web dev, chances are you've heard the saying, "everything in JavaScript is an object!". That isn't entirely true, but we're not here today to discuss technicalities, or even this "truism" at all. Instead, we're going to be discussing regular old, boring objects. The type you might use to store and cart around data in your code, and some techniques about how you might wrangle them more effectively.

A brief recap on objects

Objects are fairly straightforward. Their basic structure consists of zero or more key-value pairs (a.k.a. object properties) inside a set of braces. The value can be anything supported by JavaScript, including functions and other objects as well. More complex object structures may include several layers of nested objects, or even circularly have itself as a value (not great if you want to display the data).

One way of thinking about objects is to liken them to a computer's directory structure. The key might represent a file or folder, the value its contents. A nested object would be a directory within a directory, a function an executable, and a circular reference a shortcut!

const Documents = {
  "My Contacts.csv": [["Mum", 61412345678], ["Domino's", 82752120]],
  "password.txt": "hunter2",
  "New Folder": {},
  Work: {
    "cleanup_script.ahk": () => {},
    "Invoices": {
      "2018": {
        /* ...etc */
      },
      "2019": {
        /* ...etc */
      },
      "2020": {
        "Invoice0331.docx": ""
        /* ...etc */
      }
    }
  }
};

N.B. outside of the above example, it's usually preferred for variables to be named in camelCase rather than PascalCase

Similar to how the directory path to Invoice0331.docx would be /Documents/Work/Invoices/2020/Invoice0331.docx, you would describe it in JavaScript like so:

Documents.Work.Invoices[2020]["Invoice0331.docx"]

We can use the period for property names that are valid JavaScript identifiers; for all other ones (including ones with whitespace, periods, other funky stuff) we have to use the brackets.

Optional chains, more like compulsory ropes

Continuing with our directory analogy, what happens if you try to access a file or folder that doesn't exist? Say you made a typo and tried to open Documents/Work/Invoices/2021/OBVIOUSLYDOESNTEXIST—your CLI would throw a complaint, or if using a GUI file explorer, you might get an alert. Likewise, JavaScript would give you the following error if you tried to access Documents.Work.Invoices[2021].OBVIOUSLYDOESNTEXIST:

Uncaught TypeError: Cannot read property 'OBVIOUSLYDOESNTEXIST' of undefined

When coding, trying to access a property value whose intermediary node does not exist is a fairly common occurrence. Perhaps you may have tried to access a ref that hasn't been set yet, or a particular dataset's shape may not be fully complete. If you are aware that a certain property may not exist, you might decide to test it first before trying to go through with the full access, essentially the coding equivalent of dipping your toes in the water:

// given b may or may not be defined
if (!a.b) return;
return a.b.c;

// or
return a.b && a.b.c;

That works ok, but you can quickly see how this might become a nuisance:

return (
  someObj.someNestedObj &&
  someObj.someNestedObj.someOtherObj &&
  someObj.someNestedObj.someOtherObj.someFinalPropertyValue
  // etc
);

For this, optional chains work better. Optional chaining is fairly new, only having been moved to stage 4 of its ECMAScript proposal in early December 2019. It is very simple to use—just use ?. in place of . after the property you wish to test. This also works for method calls you are not certain are defined and even for array indices:

// check if myObj.prop1 is defined before trying to access prop2
myObj.prop1?.prop2; // will give undefined if either prop1 or prop2 doesn't exist

// check if myObj.prop1 is defined before trying to access "prop 2"
myObj.prop1?.["prop 2"];

// check if myObj.method is defined before trying to call it
myObj.method?.();

// check if myArr[5] is defined before trying to access its 8th index
myArr[5]?.[8];

If the property you tested with the optional chaining operator is nullish—either undefined or a null value—the chain short-circuits and evaluates as undefined. This has the same effect as using a logical AND operator &&, but in fewer lines. With deeply nested objects or extraordinarily long key names, this small change is great for readability.

// instead of
if (
  (object.that.is && object.that.is.deeply.nested) ||
  (object.withExtraordinarily &&
    object.withExtraordinarily.longPropertyKeyNames)
) {
  /* code */
}

// you can write
if (
  object.that.is?.deeply.nested ||
  object.withExtraordinarily?.longPropertyKeyNames
) {
  /* code */
}

// of course you could write this as an alternative
const is = object.that.is;
const withE = object.withExtraordinarily;
if ((is && is.deeply.nested) || (withE && withE.longPropertyKeyNames)) {
  /* code */
}
// but you've unnecessarily introduced variables

Destructuring is fun

Destructuring isn't anything new or revolutionary; it's been around for a while and it's great fun.

Destructure a property!

// instead of
const apples = fruit.apples;

// consider
const { apples } = fruit;

Destructure a nested property!

// instead of
const apples = food.fruit.apples;

// consider
const { apples } = food.fruit;

// or
const {
  fruit: { apples }
} = food;

Destructure multiple properties!

// instead of
const apples = food.fruit.apples;
const vegetables = food.vegetables;

// consider
const {
  fruit: { apples },
  vegetables
} = food;

Destructure and rename your properties!

// instead of
const apples = food.fruit.apples;
const veggies = food.vegetables;

// consider
const {
  fruit: { apples },
  vegetables: veggies
} = food;

Destructure your React props!

//instead of
const Pantry = props => {
  const apples = props.food.fruit.apples;
  const vegetables = props.food.vegetables;
  const handleClick = props.onClick;

  /* react code */
};

// consider
const Pantry = (
  {
    food: {
      fruit: { apples },
      vegetables: veggies
    },
    onClick: handleClick
  },
) => {
  /* react code */
};

Truly, the possibilities are endless.

Optional... destructuring?

Destructuring really shines when you need to use a substantial number of properties from one or more objects, e.g. if you have many React component props. It saves you the trouble of having to define each variable, one chain expression at a time. But with nested objects, you may once again run into the issue of undefined intermediaries.

At first glance, there is no immediately obvious way of using optional chaining operators with destructuring. However, because optional chains evaluate to undefined when they short-circuit, it is entirely possible to take advantage of their use in destructuring by combining them with default and substitute values:

// this is equivalent
const prop2 = obj?.prop1.prop2;

// to this
const { prop2 } = obj?.prop1 || {}

By short-circuit evaluating obj?.prop1 to undefined, you can substitute the left-hand-side with an empty object {} using the logical OR operator ||. If so desired, the substitute could be an object with any shape custom to your needs.

This principle can be applied to a wide variety of scenarios, both with or without optional chaining altogether:

// this is equivalent
const prop2 = obj?.prop1.prop2?.prop3;

// to this
const { prop2: { prop3 } = {} } = obj?.prop1 || {};

In summary

Not every situation will call for optional chaining, destructuring, or "optional destructuring". It is important to be cognizant of when and where you choose to employ ?. instead of ., only in front of properties you genuinely need to test and not as a wholesale replacement. In some cases, it may be easier and more readable to write out a few chain expressions than to deal with destructuring and short-circuiting to default/substitute values.

In the end though, these expressions are great fun and feel natural to use, all that's left is to employ them where you see fit.

Posted on by:

jackshen profile

Jack Shen

@jackshen

Sydney-based senior front-end engineer. I blogpost my personal and technical experiences for whoever decides to stop by. Thanks for reading!

Discussion

markdown guide
 

?. lead us to hide our problem?

My favourite solution is standard deconstruction

// this is equivalent
const prop2 = obj?.prop1.prop2?.prop3;

// to this
const {prop1:{prop2:{prop3=null}={}}={}} = obj || {}  // too much bracket
if (prop3===null) return

but, mainly I try avoid this deep options

 

Great comprehensive writeup Jack.

A good trick for accessing data in deeply nested structures is using the power of JavaScript's lambdas.

You can think of passing functions around as passing scope around.

/*
@func
return the value at the end of the obj or arr chain
- if any of the vals or arr accessors along the chain don't exist
- then return null

@param {() => *} fn - the lambda wrapping the nested call, via dot notation
@return {*|null} - the val in the key-val pair at the end of the dot notation chain
*/
export const getVal = fn => {
  try {
    return fn();
  } catch {
    return null;
  }
};

//@tests
const obj = { manuf: "Acme" };
const val = getVal(() => obj.k1.k2.k3.arrOfObjs[0].k5.k6.trim().toLowerCase().length);
console.log(val);
/*
note:
this example returns null since this path doesn't exist in the obj
*/
 

Thanks for sharing I was forgetting about optional chaings. btw That folder structure analogy is a good approach to teach objects.