I don't understand about spread syntax inside objects

I don't understand about spread syntax inside objects.

console.log(...false) // TypeError not iterable
console.log(...1) // TypeError not iterable
console.log(...null) // TypeError not iterable
console.log(...undefined) // TypeError not iterable

I understand above codes that occurs error because of none-iterator.

But these codes are working well.

console.log({...false}) // {}
console.log({...1}) // {}
console.log({...null}) // {}
console.log({...undefined}) // {}

Please let me know why the above codes are working.


Solution 1:

There is no spread operator!

This is quite important in order to understand what's happening, so I have to start with it.

There is no spread operator defined in the language. There is spread syntax but as a sub-category of other types of syntax. This sounds like just semantics but it has a very real impact on how and why ... works.

Operators behave the same way every time. If you use the delete operator as delete obj.x, then you always get the same result regardless of context. Same with typeof or perhaps even - (minus). Operators define an action that will be done in the code. It's always the same action. Someimes operators might be overloaded like +:

console.log("a" + "b"); //string concatenation
console.log(1 + 2);     //number addition

But it still doesn't vary with the context - where you put this expression.

The ... syntax is different - it's not the same operator in different places:

const arr = [1, 2, 3];
const obj = { foo: "hello", bar: "world" };

console.log(Math.max(...arr));   //spread arguments in a function call
function fn(first, ...others) {} //rest parameters in function definition
console.log([...arr]);           //spread into an array literal
console.log({...obj});           //spread into an object literal

These are all different pieces of syntax that look similar and behave similar but definitely not the same. If ... were an operator, you can change the operands and still be valid but that's not the case:

const obj = { foo: "hello", bar: "world" };

console.log(Math.max(...obj)); //spread arguments in a function call
                               //not valid with objects

function fn(...first, others) {} //rest parameters in function definition
                                 //not valid for the first of multiple parameters

const obj = { foo: "hello", bar: "world" };

console.log([...obj]); //spread into an array literal
                       //not valid when spreading an arbitrary object into an array

So, each use of ... has separate rules and works not like any other use.

The reason is simple: ... is not one thing at all. The language defines syntax for different things, like function calls, function definitions, array literals, and objects. Let's focus on the last two:

This is valid syntax:

const arr = [1, 2, 3];
//          ^^^^^^^^^
//              |
//              +--- array literal syntax

console.log(arr);

const obj = { foo: "hello", bar: "world!" };
//          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//                         |
//                         +--- object literal syntax

console.log(obj);

But these aren't:

const arr = [0: 1, 1: 2, 2: 3];
//invalid - you cannot have key-value pairs

const obj = { 1, 2, 3 };
//invalid - you need key-value pairs

Not surprising - different syntax has different rules.

Again, the same applies to using ...[...arr] and {...obj} are just two different types of code you can use in JavaScript but there is no overlap between the ... usages, just how you can use 1 both as [1] and { 1: "one" } but it's not the same meaning both times.

What actually happens when you use spread in function call and spread into an object?

This is the real question that needs answering. After all, these are different operations.

Your sample with console.log(...false) and console.log({...false}) demonstrate a function call and an object literal usage in particular, so I'll talk about those two. Just as a note, an array literal spread syntax [...arr] would behave very similar in terms of what is valid and what isn't but it's not quite relevant here. The important thing is why objects behave differently, so we just need one example to compare against.

Function call spread fn(...args)

The specs don't even have a special name for this construct. It's just a type of ArgumentList and in section 12.3.8.1 Runtime Semantics: ArgumentListEvaluation (ECMAScript language specification link) it defines essentially "If the argument list has ... then evaluate the code like this". I'll save you the boring language used in the specs (feel free to visit the link, if you want to see it).

The key point from the steps to be taken is that with ...args the engine will try to get the iterator of args. In essence that is defined by the iteration protocol (MDN link). For that, it will try calling a method defined with @@iterator (or @@asyncIterator). This is where you get a TypeError — it happens when args doesn't expose such a method. No method, means it's not an iterable, and thus the engine cannot continue calling the function.

Just for completeness, if args is an iterable, then the engine will step through the entire iterator until exhausted and create the arguments from the results. That means that we can use any arbitrary iterable with spread syntax in function calls:

const iterable = {
  [Symbol.iterator]() { //define an @@iterator method to be a valid iterable
    const arr = ["!", "world", "hello"];
    let index = arr.length;
    
    return {
      next() { //define a `next` method to be a valid iterator
        return { //go through `arr` backwards
          value: arr[--index],
          done: index < 0
        }
      }
    }
  }
}

console.log(...iterable);

Object spread {...obj}

There is still no special name for this construct in the specs. It's a type of PropertyDefinition for an object literal. Section 12.2.6.8 Runtime Semantics: PropertyDefinitionEvaluation (ECMAScript language specification link) defines how this is to be processed. I'll spare you the definition again.

The difference comes in how exactly the obj element is handled when spreading its properties. To do that, the abstract operation CopyDataProperties ( target, source, excludedItems ) (ECMAScript language specification link) is performed. This one is probably worth reading to better understand exactly what happens. I'll just focus on the important details:

  1. With the expression {...foo}

    • target will be the new object
    • source will be foo
    • excludedItems will be an empty list, so it's inconsequential
  2. If source (reminder, this is foo in the code) is null or undefined the operation concludes and target is returned from the CopyDataProperties operation. Otherwise, continue.

  3. Next important thing is that foo will be turned into an object. This will use the ToObject ( argument ) abstract operation which is defined like this (reminder again that you won't get null or undefined here):

Argument Type Result
Undefined Throw a TypeError exception.
Null Throw a TypeError exception.
Boolean Return a new Boolean object whose [[BooleanData]] internal slot is set to argument. See 19.3 for a description of Boolean objects.
Number Return a new Number object whose [[NumberData]] internal slot is set to argument. See 20.1 for a description of Number objects.
String Return a new String object whose [[StringData]] internal slot is set to argument. See 21.1 for a description of String objects.
Symbol Return a new Symbol object whose [[SymbolData]] internal slot is set to argument. See 19.4 for a description of Symbol objects.
BigInt Return a new BigInt object whose [[BigIntData]] internal slot is set to argument. See 20.2 for a description of BigInt objects.
Object Return argument.

We'll call the result of this operation from.

  1. All own properties in from that are enumerable are written to target with their values.

  2. The spread operation completes and target is the new object defined using the object literal syntax. Finished!

To summarise even more, when you use spread syntax with an object literal, the source that is being spread will be turned into an object first, and then only own enumerable properties will actually be copied onto the object being instantiated. In the case of null or undefined being spread, the spreading is simply a no-op: no properties will be copied and the operation completes normally (no error is thrown).

This is very different from how spreading works in function calls, as there is no reliance on the iteration protocol. The item you spread does not have to be an iterable at all.

Since the primitive wrappers like Number and Boolean don't produce any own properties, there is nothing to copy from them:

const numberWrapper = new Number(1);

console.log(
  Object.getOwnPropertyNames(numberWrapper),       //nothing
  Object.getOwnPropertySymbols(numberWrapper),     //nothing
  Object.getOwnPropertyDescriptors(numberWrapper), //nothing
);

const booleanWrapper = new Boolean(false);

console.log(
  Object.getOwnPropertyNames(booleanWrapper),       //nothing
  Object.getOwnPropertySymbols(booleanWrapper),     //nothing
  Object.getOwnPropertyDescriptors(booleanWrapper), //nothing
);

However, a string object does have own properties and some of them are enumerable. Which means that you can spread a string into an object:

const string = "hello";

const stringWrapper = new String(string);

console.log(
  Object.getOwnPropertyNames(stringWrapper),       //indexes 0-4 and `length`
  Object.getOwnPropertySymbols(stringWrapper),     //nothing
  Object.getOwnPropertyDescriptors(stringWrapper), //indexes are enumerable, `length` is not
);

console.log({...string}) // { "0": "h", "1": "e", "2": "l", "3": "l", "4": "o" }

Here is a better illustration of how values would behave when spread into an object:

function printProperties(source) {
  //convert to an object
  const from = Object(source);
  
  const descriptors = Object.getOwnPropertyDescriptors(from);
  
  const spreadObj = {...source};

  console.log(
  `own property descriptors:`, descriptors,
  `\nproduct when spread into an object:`, spreadObj
  );
}

const boolean = false;
const number = 1;
const emptyObject = {};
const object1 = { foo: "hello" };
const object2 = Object.defineProperties({}, {
  //do a more fine-grained definition of properties
  foo: {
    value: "hello",
    enumerable: false
  },
  bar: {
    value: "world",
    enumerable: true
  }
});

console.log("--- boolean ---");
printProperties(boolean);

console.log("--- number ---");
printProperties(number);

console.log("--- emptyObject ---");
printProperties(emptyObject);

console.log("--- object1 ---");
printProperties(object1);

console.log("--- object2 ---");
printProperties(object2);

Solution 2:

The object spread is quite different. It maps to Object.assign() internally.

So const a = {...1} is same as const a = Object.assign({}, 1) Here Object.assign({},1) has treated 1 as object not as number. Therefore, you did not get any exception thrown.

Additionally if you have tried same thing for arrays [...1] it should have thrown error, since it does not treats 1 as object and you get the same behavior as ..1.

To summarize:

console.log({...false}) => console.log(Object.assign({}, false))
console.log({...1}) => console.log(Object.assign({}, 1))
console.log({...null}) => console.log(Object.assign({}, null))
console.log({...undefined}) => console.log(Object.assign({}, undefined))

PS: Object.assign() spec

Solution 3:

Well this is the beauty of JS amongst may other things, it is due to iterable protocol. By virtue this means it means array or map. By default both of these have the behaviour assigned in language construct that it is a group of items that we can iterate over one by one. We can also count and add and delete items as needed.

EXAMPLE.JS by default understand them as set of series or set or group what ever.

const array1 = [1, 4, 9, 16];
console.log(array1.length);
array1.push(5);
console.log(array1.length);

Now these are not the only type of iterateable objects in JS so are strings.

string = 'abc';
console.log(string.length)
string = string+'d';
console.log(string.length)
console.log(string[3])

then there are array like objects that can also be iterated

let arrayLike = {
  0: "Hello",
  1: "World",
};
console.log(arrayLike[1])
Now lets understand instance of your second example with an example run the below code console.log with throw error. Because objects by default do not have iterate behaviour unlike arrays and array like objects. Since spread operator states to consider any after three dots as array if it fits the construct. so {...false} does almost what happens to b in below example. It remains an empty object WHY because objects need key value paring.

a = [1,2,3];
b={1,2,3};


console.log(a[1]);
console.log(b[1]);

a does does not need pairing key value definition it does this by default on its own as widely known as index.

a = [4,5,6];
b={1:4,2:5,3:6};


console.log(a[1]);
console.log(b[1]);
on the same note read this example

a = [1,2,3];
b=[4,5,6];
c= [...a,...b];
d = [...a,b[1]];

console.log(c);
console.log(d);

...(three dots) only tell Js to consider this as array if its iteratable else just throw error. true false are not iteratable, neither are objects in curly brackets. thats is why object remained blank since ... wont work on non iteratbale items. This works

a = [1,2,3];
b = {...a};
console.log(b)

This does not - kaboom

a = [...false];

This doesn't work either but just stays silent - shshshs

a = {...false};

I hope you got the point. Any thing else just pop subsequent question.