DEV Community

Cover image for 👋 Say Goodbye to Spread Operator: Use Default Composer

👋 Say Goodbye to Spread Operator: Use Default Composer

Aral Roca on June 05, 2023

Original article: https://aralroca.com/blog/default-composer When working with objects in JavaScript, it is common to need to set default values f...
Collapse
 
renekaesler profile image
René Kaesler • Edited

For all the devs using lodash:
merge & mergeWith is doing the similar thing

Collapse
 
spock123 profile image
Lars Rye Jeppesen

No dev should be using lodash

Collapse
 
insidewhy profile image
insidewhy

What's the reason? With lodash-es and a tree-shaking bundler I only pay for what I'm using, so I don't see the harm in importing a few functions from lodash if I need them.

Thread Thread
 
rokuem profile image
Mateus Amorim • Edited

lodash has bad types in general, destroying typesafety and can be easily overused, creating very ugly code that is hard to understand unless you are really familiar with it. Many of the things it does can easily be done with native javascript too.

ofc, you can still use it for some things, and you can wrap it with functions with better typings, but in general it is good to avoid using it at all when working on teams since others might overuse it or use it in an unsafe manner.

something that is commonly oversued is the lodash "get" method, ex:

_.get(a, "b.c", [])
Enter fullscreen mode Exit fullscreen mode

this has many issues:
1 - the second parameter is not typed, so we don't know what we can access or if it exists or not
2 - the third parameter is not restricted to the type of a.b.c
3 - the resulting type is "any", making it easy to do typeErrors.

Nowdays we have better alternatives in general, for example:

a?.b?.c ?? []
Enter fullscreen mode Exit fullscreen mode

this will solve all of the issues mentioned before and doesn't rely on a library for it :D

Thread Thread
 
grsmto profile image
Adrien Denat

And there is Remeda!

Thread Thread
 
insidewhy profile image
insidewhy • Edited

I agree with that, there's bad stuff in lodash for sure (I never use get so it's good you point out the problems with it), but there's okay stuff too.

Collapse
 
mikethai profile image
Mike Thai

Why not? I think lodash is totally fine if you use it efficiently and understand what it does.

But if you're one of those devs who imports the entire library into every project to only use like 2% of it and don't know how any of it works/are completely helpless at your job without it, then yeah I agree - you're just shooting yourself in the foot.

Thread Thread
 
spock123 profile image
Lars Rye Jeppesen

Because lodash is not maintained as a project any longer afaik

Thread Thread
 
twiddler profile image
twiddler
Thread Thread
 
aneesshameed profile image
aneesshameed

Lodash is a matured library. Because its not updated, that doesn't means that it cannot be used on projects. Lodash works and it helps to write clean code.

Thread Thread
 
aplombomb profile image
Christian Baldwin

Our definitions of clean code are obviously different.

My definition of clean code is thoughtful composition of logic and data in order to avoid needing to depend on 3rd party libraries to help you maintain it.

Collapse
 
renekaesler profile image
René Kaesler • Edited

Hehe, okay okay!

I had not planned to trigger a framework debate. I'm sure your preferred FP-utility framework is capable of the merging concept, too. No matter if it's ramda, underscore or whatever else exists.

Collapse
 
dchowitz profile image
Denny Christochowitz

🤣

Collapse
 
aralroca profile image
Aral Roca

Thank you for sharing @renekaesler. At first we got the same functionality with mergeWith:

github.com/aralroca/default-compos...

In the end we opted for our own implementation to avoid 10kb, even though we were importing only mergeWith and not all lodash we thought it was too much for this feature.

This is the current implementation:

github.com/aralroca/default-compos...

Collapse
 
renekaesler profile image
René Kaesler • Edited

Thanks for sharing your lib to the community, @aralroca !

Got your point! But 10kb for a single method import sounds strange. Just being curious: Have you used the lodash-es package?

All in all it's not about lodash. It's just about the hint: Widely used utility frameworks may already have this functionally available 😊

Collapse
 
phylis profile image
Phylis Jepchumba, MSc

Welcome to the community, René Kaesler! We're thrilled to have you here. It's always exciting to see new members join, bringing fresh perspectives and ideas. Feel free to explore the various discussions, ask questions, and share your knowledge with others. Don't hesitate to reach out if you need any assistance or have any specific interests within the community. We're here to support each other and learn together. Once again, a warm welcome to you, and we look forward to connecting with you further. Happy participating!

Collapse
 
aneesshameed profile image
aneesshameed

merge and mergeWith is not doing what this post is about. Completely different

Collapse
 
renekaesler profile image
René Kaesler • Edited

I should have given an example. Normally I am using the following pattern, for default nested options:

const options = {
  b: 2,
  c: 3,
  nested: {
    e: 5,
    f: 6,
  },
};

const defaultOptions = {
  a: 1,
  b: "two",
  nested: {
    d: 4,
    e: "five",
  },
};

// this mutates `defaultOptions`:
const composedOptions = _.merge(defaultOptions, options);

// this preserves `defaultOptions`:
// const composedOptions = _.merge({}, defaultOptions, options);

console.log(composedOptions);
// => { a: 1, b: 2, c: 3, nested: { d: 4, e: 5, f: 6 }}




Enter fullscreen mode Exit fullscreen mode

If you want to tweak the behaviour of value replacement differently for some values, you can use mergeWith. It's quite similar what the library is accomblishing.

Collapse
 
dtinth profile image
Thai Pangsakulyanont

Did you mean defaultsDeep?

npmjs.com/package/lodash.defaultsdeep

Collapse
 
renekaesler profile image
René Kaesler

No, I really meant to use merge (see my comment before).

But your hint is semantically even better! 🥳
Didn't know about defaultsDeep

Collapse
 
jericbas profile image
Jeric Bas • Edited

Nah! Avoid lodash merge and mergeWith.

Collapse
 
blafasel3 profile image
Maximilian Lenhardt

At least I encountered memory leaks using lodash methods and it's not maintained since 2018 or so.

Collapse
 
ghamadi profile image
Ghaleb • Edited

The problem you propose is less common than the one where the spread operator is enough.

Also, when you need to overwrite values in "huge, heavily nested objects" AND treat some keys differently from others, you make it maintainable by passing the object down several functions.

I would say a pipeline of functions is easy to maintain and most likely easier to read than the setConfig which should theoretically grow in proportion to the size and complexity of your object.

It's a nice project, honestly, but what I dislike is the title. There's no ground for words like "Say Goodbye to Spread Operator".

When the spread op is enough (which is most cases), I have to assume that it will be more performant than a function that applies deep overwriting, and I still need convincing that the example you proposed is the best approach. For instance, why do you assume the need to have a defaults object that holds everything in the original object? The main reason for the complication comes from that IMO.

Collapse
 
aralroca profile image
Aral Roca

You are right, the title may be confusing, the spread operator is great and should not always be replaced by defaultComposer, just in case you need to mix defaults in a nested way.

Could you show an example of the pipelines you propose? I'm curious and if we can make the setConfig more user friendly it will be very welcome. Thanks 😊

Collapse
 
imadhy profile image
Imad El Hitti

While everyone has to come with his own science which sometimes is useless, I'll come with a big thank because I see a real benefit in using this library for testing actually. We have a lot of cases where you have this default object and for each test scenario we have to replace a small part of that object for testing purpose.
Thank again !

Collapse
 
effinrich profile image
Rich Tillman

Great point, would be a time saver for sure.

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.

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
 
romantictrust profile image
Uladzislau Tsyrkunou

Say Goodbye to libraries: Use native developer.mozilla.org/en-US/docs/W...

Collapse
 
aralroca profile image
Aral Roca

Structured clone is a deep clone, but not for the defaults

Collapse
 
charlesr1971 profile image
Charles Robertson • Edited

The only issue that I have with default-composer is that if you have an object property that is an array, with a single/multiple values and you merge with another array, the original array values are overwritten.
The same does not happen for new object properties. These are preserved, and added to the resulting object.
This seems to be an inconsistency. I would like to see a config object that allows the behaviour of the way arrays are merged, to be altered?

You could have 4 different modes:

Mode 1:

Overwrite DEFAULT


//original 
const original = {
    prop1: [‘a’,’e’,’f’]
}

//defaults
const defaults = {
    prop1: [‘a’,’b’,’c’]
}

const config = {
    arrayMode: ‘overwrite’
}

const results = defaultComposer(
    defaults, 
    original,
    config
);

//OR

const results = defaultComposer(
    defaults, 
    original
);

//results
{
    prop1: [‘a’,’b’,’c’]
}

Enter fullscreen mode Exit fullscreen mode

Mode 2:

Prepend


//original 
const original = {
    prop1: [‘a’,’e’,’f’]
}

//defaults 
const defaults = {
    prop1: [‘a’,’b’,’c’]
}

const config = {
    arrayMode: ‘prepend’
}

const results = defaultComposer(
    defaults, 
    original,
    config
);

//result 
{
    prop1: [‘a’,’e’,’f’,‘a’,’b’,’c’]
}

Enter fullscreen mode Exit fullscreen mode

Mode 3:

Append


//original 
const original = {
    prop1: [‘a’,’e’,’f’]
}

//defaults
const defaults = {
    prop1: [‘a’,’b’,’c’]
}

const config = {
    arrayMode: ‘append’
}

const results = defaultComposer(
    defaults, 
    original,
    config
);

//result 
{
    prop1: [‘a’,’b’,’c’,‘a’,’e’,’f’]
}

Enter fullscreen mode Exit fullscreen mode

Mode 4:

Index


//original 
const original = {
    prop1: [‘a’,’b’,’c’,’d’,’e’,‘f’,’g’,’h’]
}

//defaults
const defaults = {
    prop1: [‘f’,’g’,’h’]
}

const config = {
    arrayMode: ‘index’
}

const results = defaultComposer(
    defaults, 
    original,
    config
);

//result 
{
    prop1: [‘f’,’g’,’h’,’d’,’e’,‘f’,’g’,’h’]
}

//Duplicates config key

const config = {
    arrayMode: ‘index’,
    arrayDuplicates: false
}

const results = defaultComposer(
    defaults, 
    original,
    config
);

//result 
{
    prop1: [‘f’,’g’,’h’,’d’,’e’]
}

Enter fullscreen mode Exit fullscreen mode
Collapse
 
aralroca profile image
Aral Roca

thanks for your comment. This is happening because is not a fully merge, is detecting defaultables to replace it, and is replacing to the defaults only if the value is undefined, null, empty string, empty array, empty object. However as you comment we can add this feature configurable, is a good idea. I'm going to create the issue to reminder this. Thanks for your contribution

Collapse
 
charlesr1971 profile image
Charles Robertson

Anyway, great little library. Thanks 🙏

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
 
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
 
llorx profile image
Jorge Fuentes

As always: "Forget about JavaScript, use yet-another-library".

Collapse
 
ericmoir profile image
EricMoIr

If you have such complex objects that need to be created multiple times and modified (otherwise default values don't make sense) you probably should be using classes instead of spread anyway

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
 
ericis profile image
Eric Swanson

What are the performance implications of these choices?

Collapse
 
aralroca profile image
Aral Roca

It has complexity O(|S1 ∪ S2|). It traverses through all keys of both objects without duplicates. If we have { a: 1, b: 2} and {b: 8, c: 9} we go through the keys "a", "b" and "c".

Collapse
 
joelbonetr profile image
JoelBonetR 🥇

Isn't makestruct way easier to use while accomplishing the same goal?

Cheers!

Collapse
 
joshmgca profile image
josh_CA

Looks like immer but native of js, nice!!

Collapse
 
satel profile image
Satel

All good point! But installing 3rd party to manage this deserve at some case but not some other cases. So only smart using would be the solution, not saying Good Bye :)

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
 
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
 
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
 
t_bogard profile image
Erick Rodriguez

What is the difference with lodash.copydeep? It does deep copies of objects as well?

Collapse
 
aralroca profile image
Aral Roca

cloneDeep of lodash is like structuredClone, is returning an exact copy of the object but with a different reference in memory.

With defaultComposer you can merge different objects with priority, taking the other ones as default values when the data is not comming.

// cloneDeep
const clonedObject = _.cloneDeep(object) // Exact copy of the object but with a different reference in memory

const mixOfObjects = defaultComposer(objectPriorityN, objectPriority2, objectPriority1)
Enter fullscreen mode Exit fullscreen mode
Collapse
 
miketalbot profile image
Mike Talbot ⭐ • Edited

And also your library supports overwriting empty values (e.g. "") as well as undefined, doing that with any kind of merge would probably end up with empty values left in the output.

Collapse
 
killea profile image
Hank Wang

looks like another shit