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.
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"
};
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"],
};
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"
**/
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,
};
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,
});
Maintaining this can be very tidious, especially with huge, heavily nested objects.
With defaultComposer
we could only use this:
import defaultComposer from 'default-composer'; // 300B
// ...
const results = defaultComposer(defaults, original);
Easier to maintain, right? π
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;
},
});
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.
Top comments (53)
For all the devs using lodash:
merge
&mergeWith
is doing the similar thingNo dev should be using lodash
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.
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:
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:
this will solve all of the issues mentioned before and doesn't rely on a library for it :D
And there is Remeda!
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.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.
Because lodash is not maintained as a project any longer afaik
github.com/lodash/lodash/issues/5605
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.
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.
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.
π€£
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...
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 π
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!
merge and mergeWith is not doing what this post is about. Completely different
I should have given an example. Normally I am using the following pattern, for default nested options:
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.Did you mean
defaultsDeep
?npmjs.com/package/lodash.defaultsdeep
No, I really meant to use
merge
(see my comment before).But your hint is semantically even better! π₯³
Didn't know about
defaultsDeep
Nah! Avoid lodash
merge
andmergeWith
.At least I encountered memory leaks using lodash methods and it's not maintained since 2018 or so.
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 theoriginal
object? The main reason for the complication comes from that IMO.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 π
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 !
Great point, would be a time saver for sure.
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.
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.
Say Goodbye to libraries: Use native developer.mozilla.org/en-US/docs/W...
Structured clone is a deep clone, but not for the defaults
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
Mode 2:
Prepend
Mode 3:
Append
Mode 4:
Index
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
Anyway, great little library. Thanks π
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:
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:
Not extensively tested yet, if some cases are not handled properly, please leave a note.
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
As always: "Forget about JavaScript, use yet-another-library".