Why does [...user] throw a TypeError when user is a plain object, while {...user} copies its properties without complaint?
answer
- three dots, two different operations
- one asks for a sequence
- the other copies own properties
- no Symbol.iterator on Object.prototype
- Object.entries gives you a real array
basics
~20 sArray spread runs the iteration protocol and a plain object has no Symbol.iterator method, so it throws. Object spread does not iterate at all — it copies the source's own enumerable properties onto the new object.
solid answer
~40 sThey are two different syntaxes that happen to share three dots. `[...user]` — like `f(...user)` — is *array/argument spread*, which asks for `user[Symbol.iterator]()`, and since `Object.prototype` defines no such method the engine throws a `TypeError` along the lines of "user is not iterable". `{...user}` is *object spread*, added in ES2018, and it never touches the iteration protocol: it copies the source's own enumerable string and symbol properties into the new literal, so it works on any object, including one with no properties at all. The practical consequence is that to get an array out of a plain object you pick the shape you want explicitly — `Object.keys(user)`, `Object.values(user)` or `Object.entries(user)`, all of which return real arrays. Alternatively, if the object is genuinely a collection, give it a `Symbol.iterator` method and `[...user]` starts working.
code
javascript · 10 linesconst user = { id: 1, name: 'Ada' };
console.log({ ...user }); // { id: 1, name: 'Ada' }
console.log(Object.entries(user)); // [['id', 1], ['name', 'Ada']]
try {
console.log([...user]);
} catch (e) {
console.log(e instanceof TypeError); // true
}go deeper
Recall that plain objects cannot be spread into an array and that Object.keys, Object.values or Object.entries is the way to get a list of an object's contents.
Explain the mechanism: array spread invokes Symbol.iterator, object spread copies own enumerable string and symbol properties. Mention that object spread is a shallow copy and tolerates nullish sources.
Read a 'not iterable' TypeError back to its cause quickly — a record where a list was expected, or an undefined value — and decide whether the right fix is converting at the boundary or implementing the protocol on the type.
Weigh whether domain types should implement Symbol.iterator at all. Making a type iterable is a public contract every consumer will rely on; a conversion helper at the edge is often the smaller commitment.
## Same three dots, two unrelated operations The `...` token appears in several places in the grammar and does a different job in each. Two of them look alike and behave nothing alike. **Array and argument spread** — `[...x]`, `f(...x)`, `new C(...x)` — is defined in terms of iteration. The engine gets an iterator from `x` and drains it, appending each produced value. **Object spread** — `{ ...x }` inside an object literal, standardised in ES2018 — is defined in terms of *property copying*. It performs the same abstract operation as `Object.assign` on the target: it reads the source's own enumerable string-keyed and symbol-keyed properties and defines matching own properties on the new object. One asks "can I walk you as a sequence?", the other asks "what own properties do you have?". A plain object answers the second question happily and cannot answer the first at all. ## Why the array form throws Spreading into an array is roughly: ```js const out = []; const it = user[Symbol.iterator](); // TypeError here for a plain object let step; while (!(step = it.next()).done) out.push(step.value); ``` `user[Symbol.iterator]` evaluates to `undefined`, calling `undefined` is a `TypeError`, and it is thrown before a single element is produced. The exact message text is engine-specific — V8 says `user is not iterable` — so assert on the error type, not the string. The same failure hits every other iteration consumer for the same reason: `for (const x of user)`, `const [a] = user`, `new Set(user)` and `Promise.all(user)` all throw on a plain object. ## Why the object form does not ```js const user = { id: 1, name: 'Ada' }; const copy = { ...user }; // { id: 1, name: 'Ada' } const empty = { ...42, ...null }; // {} — no properties to copy, no error ``` Object spread tolerates primitives and even `null`/`undefined` sources: there is simply nothing to copy. It is a *shallow* copy — nested objects are shared by reference — and it copies values, so a getter on the source is invoked once and its result stored as a plain data property on the target. Non-enumerable properties and anything inherited from the prototype chain are skipped. ## Getting an array out of a plain object The three static helpers each answer a different question, and all three return genuine arrays that spread and loop normally: ```js Object.keys(user); // ['id', 'name'] Object.values(user); // [1, 'Ada'] Object.entries(user); // [['id', 1], ['name', 'Ada']] ``` Note that these consider only *own enumerable string-keyed* properties, so symbol keys and inherited properties are excluded — a different filter from what array spread would have applied had the object been iterable, and worth stating out loud in an interview. ## Making the object itself iterable If the object models a collection rather than a record, the honest fix is to implement the protocol. The cheapest implementation delegates to something already iterable: ```js const bag = { items: ['a', 'b'], [Symbol.iterator]() { return this.items[Symbol.iterator](); } }; [...bag]; // ['a', 'b'] for (const x of bag) {} // works const [first] = bag; // 'a' ``` One method makes the object work with every consumer at once. Note the reverse does *not* hold: adding `Symbol.iterator` changes nothing about `{ ...bag }`, which still copies `items` and the symbol-keyed method rather than producing elements. ## The array-like near-miss A frequent follow-on is an object with a `length` and numeric keys, such as `{ length: 2, 0: 'a', 1: 'b' }`. It looks like an array, and older index-based helpers like `Array.prototype.slice.call(x)` handle it, but it has no `Symbol.iterator`, so `[...x]` still throws. `Array.from(x)` accepts it, because `Array.from` falls back to the length/index reading when the iteration protocol is unavailable. That asymmetry is the reason `Array.from` and spread are not interchangeable. ## What this tells you at a glance When you see "X is not iterable" in a stack trace, the diagnosis is mechanical: something reached for `Symbol.iterator` on a value that has none. The usual culprits are a plain object where an array was expected, a value that is `undefined` because a fetch or a destructuring default did not fire, or an API that returned a record when the caller assumed a list.
- Does adding a Symbol.iterator method to an object change what {...obj} produces?No. Object spread copies own enumerable properties and never consults the iteration protocol, so the result gains the symbol-keyed method as a property rather than producing elements. Only `[...obj]`, `f(...obj)`, `for...of` and the other iteration consumers start working.
- Why does {...null} not throw when [...null] does?Object spread treats a nullish source as having no properties to copy, so it quietly contributes nothing. Array spread must call `null[Symbol.iterator]`, which throws a `TypeError` on property access. The two forms have genuinely different tolerance for bad input.
- An object has length and numeric keys but [...obj] still throws. What works instead?`Array.from(obj)`. It uses the iteration protocol when `Symbol.iterator` exists and otherwise falls back to reading `length` and the indexed properties, so it converts array-likes that spread cannot touch. `Array.prototype.slice.call(obj)` is the older equivalent.
saying these in an interview costs you the question
- Says object spread iterates the object's keys
- Claims plain objects became iterable in ES2018
- Thinks Object.keys makes the object itself iterable
- Believes the two spread forms are the same operation
- Asserts on the engine's exact 'not iterable' message