DEV Community

Cover image for πŸ‘‹ Say Goodbye to Spread Operator: Use Default Composer
Aral Roca
Aral Roca

Posted on • Originally published at aralroca.com

πŸ‘‹ Say Goodbye to Spread Operator: Use Default Composer

Original article: https://aralroca.com/blog/default-composer

When working with objects in JavaScript, it is common to need to set default values for empty strings/objects/arrays, null, or undefined properties. When dealing with nested objects, this can become even more complicated and require complex programming logic. However, with the "default-composer" library, this task becomes simple and easy.

What is "default-composer"?

"default-composer" is a lightweight (~300B) JavaScript library that allows you to set default values for nested objects. The library replaces empty strings/arrays/objects, null, or undefined values in an existing object with the defined default values, which helps simplify programming logic and reduce the amount of code needed to set default values.



Default Composer logo


Default Composer logo


Benefits over Spread Operator and Object.assign

While ...spread operator and Object.assign() can also be used to set default values for objects, "default-composer" provides several benefits over these methods.

  • Works with nested objects, whereas the spread operator and Object.assign() only work with shallow objects.
  • More concise and easier to read than spread operator or Object.assign(). The code required to set default values with these methods can become very verbose and difficult to read, especially when dealing with nested objects.
  • More granular control over which properties should be set to default values. With spread operator and Object.assign().

Imagine we have this original object:

const original = {
  name: "",
  score: null,
  address: {
    street: "",
    city: "",
    state: "",
    zip: "",
  },
  emails: [],
  hobbies: [],
  another: "anotherValue"
};
Enter fullscreen mode Exit fullscreen mode

And these are the defaults:

const defaults = {
  name: "John Doe",
  score: 5,
  address: {
    street: "123 Main St",
    city: "Anytown",
    state: "CA",
    zip: "12345",
  },
  emails: ["john.doe@example.com"],
  hobbies: ["reading", "traveling"],
};
Enter fullscreen mode Exit fullscreen mode

We want to merge these objects replacing the original values that are "", null, [], undefined and {} to the default value. So the idea is to get:

console.log(results)
/**
 * {
 * "name": "John Doe",
 * "score": 5,
 * "address": {
 *   "street": "123 Main St",
 *   "city": "Anytown",
 *   "state": "CA",
 *   "zip": "12345"
 * },
 * "emails": [
 *   "john.doe@example.com"
 * ],
 * "hobbies": [
 *   "reading",
 *   "traveling"
 * ],
 * "another": "anotherValue"
 **/
Enter fullscreen mode Exit fullscreen mode

Probably with spread operator we will have to do something like that:

const results = {
  ...defaults,
  ...original,
  name: original.name || defaults.name,
  score: original.score ?? defaults.score, // "??" beacause 0 is valid
  address: {
    ...defaults.address,
    ...original.address,
    street: original.address.street ||Β defaults.address.street,
    city: original.address.city ||Β defaults.address.city,
    state: original.address.state ||Β defaults.address.state,
    zip: original.address.zip ||Β defaults.address.zip,
  },
  emails: original.emails.length ? original.emails : defaults.emails,
  hobbies: original.hobbies.length ? original.hobbies : defaults.hobbies,
};
Enter fullscreen mode Exit fullscreen mode

and with Object.assign something like this:

const results = Object.assign({}, defaults, original, {
  name: original.name || defaults.name,
  score: original.score ?? defaults.score, // "??" beacause 0 is valid
  address: Object.assign({}, defaults.address, original.address, {
    street: original.address.street || defaults.address.street,
    city: original.address.city || defaults.address.city,
    state: original.address.state || defaults.address.state,
    zip: original.address.zip || defaults.address.zip,
  }),
  emails: original.emails.length ? original.emails : defaults.emails,
  hobbies: original.hobbies.length ? original.hobbies : defaults.hobbies,
});
Enter fullscreen mode Exit fullscreen mode

Maintaining this can be very tidious, especially with huge, heavily nested objects.

Headache


Headache...

With defaultComposer we could only use this:

import defaultComposer from 'default-composer'; // 300B
// ...
const results = defaultComposer(defaults, original);
Enter fullscreen mode Exit fullscreen mode

Easier to maintain, right? πŸ˜‰

Easier


Happier an easier

What happens if in our project there is a special property that works differently from the others and we want another replacement logic? Well, although defaultComposer has by default a configuration to detect the defautable values, you can configure it as you like.

import { defaultComposer, setConfig } from 'default-composer';

setConfig({
  // This function is executed for each value of each key that exists in 
  // both the original object and the defaults object.
  isDefaultableValue: (
    // - key: key of original or default object
    // - value: value in the original object
    // - defaultableValue: pre-calculed boolean, you can use or not, 
    //   depending if all the rules of the default-composer library are correct
    //   for your project or you need a totally different ones.
    { key, value, defaultableValue }
    ) => {
    if (key === 'rare-key') {
      return defaultableValue ||Β value === 'EMPTY'
    } 

    return defaultableValue;
  },
});
Enter fullscreen mode Exit fullscreen mode

Conclusions

I've introduced the "default-composer" library as a solution for setting default values for nested objects in JavaScript.

The library is lightweight and provides more concise and easier-to-read code than the spread operator and Object.assign methods. It also offers more granular control over which properties should be set to default values.

In this article I provide examples of how to use the library and how it simplifies the code for maintaining nested objects.

Finally, I explain how the library can be configured to handle special cases where a different replacement logic is required. Overall, "default-composer" is a useful library for simplifying the task of setting default values for nested objects in JavaScript.

Latest comments (53)

Collapse
 
prince7195 profile image
Vijay Deepak • Edited
const original = {
  name: "",
  score: null,
  address: {
    street: "",
    city: "",
    state: "",
    zip: "",
  },
  emails: [],
  hobbies: [],
  another: "anotherValue"
};
const defaults = {
  name: "John Doe",
  score: 5,
  address: {
    street: "123 Main St",
    city: "Anytown",
    state: "CA",
    zip: "12345",
  },
  emails: ["john.doe@example.com"],
  hobbies: ["reading", "traveling"],
};
const results = { ...original, ...defaults };
// {"name":"John Doe","score":5,"address":{"street":"123 Main St","city":"Anytown","state":"CA","zip":"12345"},"emails":["john.doe@example.com"],"hobbies":["reading","traveling"],"another":"anotherValue"}
Enter fullscreen mode Exit fullscreen mode

Default spread gives the same result of "default-composer" explained in the above blog. Anyone can test the code by copy passing the code in console and test

Collapse
 
efpage profile image
Eckehard • Edited

This is a tricky one, thank you for sharing. The code on gitHub seems to be in typescript only, which is a bit verbose. Is there a JS version?

Things are pretty forward if your objects are not nested. Then you can simply use:

original = Object.assign( defaults, original)
Enter fullscreen mode Exit fullscreen mode

For the nested objects, there does indeed not seem to be something out of the box. If you donΒ΄t want another import, you could also use this routine:

// Deep nested defaults
function setDef(obj, def) {
  const isVal = (val) => Array.isArray(val) ? true : typeof val !== 'undefined'
  const isObj = (ob) => typeof (ob) === "object" && ob !== null && !Array.isArray(ob)

  for (let [key, val] of Object.entries(def)) {
    if (isObj(val)) {
      if (!(key in obj)) obj[key] = {}  // create if not exists
      obj[key] = setDef(obj[key], def[key]) // recurse -->
    }
    else if (isVal(val)) if (!isVal(obj[key])) obj[key] = val // set, if value exists and is not yet set
  }
  return obj
}

// set defaults
original = setDef(original, defaults)
console.log(original)
Enter fullscreen mode Exit fullscreen mode

Not extensively tested yet, if some cases are not handled properly, please leave a note.

Collapse
 
killea profile image
Hank Wang

looks like another shit

Collapse
 
tomascorderoiv profile image
Tomas Cordero

Oh good more tech debt so I can have more things yelling at me when this package eventually gets abandoned! The spread operator works just fine and is native.

Collapse
 
fruntend profile image
fruntend

Π‘ongratulations πŸ₯³! Your article hit the top posts for the week - dev.to/fruntend/top-10-posts-for-f...
Keep it up πŸ‘

Collapse
 
mehdi79 profile image
Mehdi mahmud • Edited

How about StructuredClone in javascript?

Collapse
 
elliot_brenya profile image
Elliot Brenya sarfo

Structuralclone?

Collapse
 
mehdi79 profile image
Mehdi mahmud

Sorry structuredClone in javascript!

Collapse
 
ritabradley_dev profile image
Rita Bradley

I think it's easy to say just use built-in JavaScript capabilities when posts suggesting a library come up, but I think that knowing there are multiple options available to achieve a desired goal is great. There's a million and one ways to do anything in the dev world; it's all about finding what works best for you or your team. So thanks for exposing me to something I didn't previously know existed.

Collapse
 
alisher profile image
Alisher-Usmonov

defu does this better

Collapse
 
wiktorwandachowicz profile image
Wiktor Wandachowicz

Wow, didn't know about defu. The github documentation looks simple and library can also be customized. Thanks a lot!

Collapse
 
joshmgca profile image
josh_CA

Looks like immer but native of js, nice!!

Collapse
 
xi_sharky_ix profile image
shArky

Another approach for mutating the original object is a library called immer. It's not helpful for setting defaults, but where time come to send this object and you wish normalize/transform values without spreading pain, I'd strongly recommend using immer.