eslint prefer-object-spread
labelled safe
Analysis Nemotron 3 Super
The rule flags uses of Object.assign with an object literal as the first argument and suggests replacing them with object spread syntax.
Fix: Replace Object.assign calls with an object literal as the first argument by spreading the remaining arguments into an object literal expression; if only one argument (the object literal), remove Object.assign entirely and keep just the object literal.
8 hazards
- Rewriting Object.assign({}, x) to ({ ...x }) changes behavior if x is null or undefined, as spread throws TypeError while Object.assign treats them as empty objects.
- If the first argument in Object.assign has properties, spreading discards them (e.g., Object.assign({a:1}, {b:2}) becomes {...{b:2}} losing {a:1}), altering the result.
- Spread syntax does not copy property descriptors (like getters/setters or non-enumerable flags), whereas Object.assign does, potentially changing object behavior.
- In loose mode or older environments, spread syntax may be a SyntaxError, causing compilation failure where Object.assign would run.
- If the spread argument contains getters, they are invoked during spread evaluation, potentially causing side effects not present in Object.assign which copies the getter function itself.
- Comments attached to the Object.assign call or its arguments may be lost or misplaced during the rewrite, especially if the autofix removes the call entirely.
- When Object.assign is used as part of a larger expression (e.g., return Object.assign({}, x)), rewriting to ({ ...x }) may alter precedence or grouping if not parenthesized correctly.
- If the first argument is a complex expression (e.g., Object.assign(getObj(), {y:2})), the fix incorrectly spreads getObj() instead of preserving the call and merging, changing evaluation and possibly causing errors if getObj() returns null.
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p002 | fix ok | This Binding Hazard | this binding: arrow vs function, methods extracted from objects, call/apply/bind |
| r1-p003 | fix ok | Getter/Setter Hazard | getters, setters and Object.defineProperty side effects |
| r1-p004 | fix ok | Temporal Dead Zone Hazard | temporal dead zone after code is hoisted or reordered |
| r1-p005 | fix ok | Sparse Array Hazard | sparse arrays, holes, Array(n) vs [n], array-likes |
| r1-p006 | fix ok | Var Hoisting Hazard | var hoisting and re-declaration inside loops or after use |
| r1-p007 | fix ok | Null/Undefined Hazard | null/undefined distinction, optional chaining short-circuit, nullish vs falsy |
| r1-p008 | fix ok | Class Fields Hazard | class fields, static blocks, private names, super calls |
| r2-p001 | fix ok | Spread Syntax Does Not Copy Property Descriptors | Spread syntax does not copy property descriptors (like getters/setters or non-enumerable flags), whereas Object.assign does, potentially changing object behavior. |
| r2-p002 | fix ok | Numeric/String Coercion with toString/valueOf | numeric/string coercion: + vs template literal, objects with toString/valueOf, Symbol, BigInt |
| r2-p003 | fix ok | Name Collisions Across Scopes After a Move | name collisions across scopes after a move (shadowing) |
| r2-p004 | fix ok | Function Declarations Inside Blocks/Switch/Labeled Statements | function declarations inside blocks, switch cases, labeled statements |
| r2-p005 | fix ok | Sparse Arrays, Holes, Array(n) vs [n] | sparse arrays, holes, Array(n) vs [n], array-likes |
| r2-p006 | silent prediction off | Complex Expression as First Argument (Evaluation Side Effects) | If the first argument is a complex expression (e.g., Object.assign(getObj(), {y:2})), the fix incorrectly spreads getObj() instead of preserving the call and merging, changing evaluation and possibly |
| r2-p007 | fix ok | Spread Syntax Does Not Copy Property Descriptors | Spread syntax does not copy property descriptors (like getters/setters or non-enumerable flags), whereas Object.assign does, potentially changing object behavior. |
| r2-p008 | fix ok | Numeric/String Coercion with toString/valueOf | numeric/string coercion: + vs template literal, objects with toString/valueOf, Symbol, BigInt |
| r2-p009 | fix ok | Name Collisions Across Scopes After a Move | name collisions across scopes after a move (shadowing) |
| r2-p010 | fix ok | Function Declarations Inside Blocks/Switch/Labeled Statements | function declarations inside blocks, switch cases, labeled statements |
| r2-p011 | fix ok | Sparse Arrays, Holes, Array(n) vs [n] | sparse arrays, holes, Array(n) vs [n], array-likes |
| r2-p012 | fix ok | Complex Expression as First Argument (Evaluation Order Side Effects) | If the first argument is a complex expression (e.g., Object.assign(getObj(), {y:2})), the fix incorrectly spreads getObj() instead of preserving the call and merging, changing evaluation and possibly |
| r2-p013 | fix ok | Precedence/Grouping in Larger Expressions | When Object.assign is used as part of a larger expression (e.g., return Object.assign({}, x)), rewriting to ({ ...x }) may alter precedence or grouping if not parenthesized correctly. |
| r2-p014 | invalid | Class Fields, Static Blocks, Private Names, Super Calls | class fields, static blocks, private names, super calls |
Sources
r1-p002 · This Binding Hazard
1const obj = {
2 value: 42,
3 method() { return this.value; }
4};
5const fn = obj.method;
6console.log(Object.assign({}, { getValue: fn }).getValue());
--- orig.js 2026-10-22 14:29:06.539162401 -0400 +++ fixed.js 2026-10-22 14:29:08.676150847 -0400 @@ -3,4 +3,4 @@ method() { return this.value; } }; const fn = obj.method; -console.log(Object.assign({}, { getValue: fn }).getValue()); +console.log(({ getValue: fn}).getValue());
undefined
r1-p003 · Getter/Setter Hazard
1let obj = {};
2Object.defineProperty(obj, 'prop', {
3 get() { return this._val; },
4 set(v) { this._val = v * 2; }
5});
6obj.prop = 5;
7const copied = Object.assign({}, obj);
8console.log(copied.prop, obj._val);
--- orig.js 2026-10-22 14:29:06.540162395 -0400 +++ fixed.js 2026-10-22 14:29:09.175148203 -0400 @@ -4,5 +4,5 @@ set(v) { this._val = v * 2; } }); obj.prop = 5; -const copied = Object.assign({}, obj); +const copied = { ...obj}; console.log(copied.prop, obj._val);
undefined 10
r1-p004 · Temporal Dead Zone Hazard
1function test() {
2 try {
3 console.log(typeof x);
4 Object.assign({}, { x: 1 });
5 let x;
6 } catch (e) {
7 console.log(e.name);
8 }
9}
10test();
--- orig.js 2026-10-22 14:29:06.541162390 -0400 +++ fixed.js 2026-10-22 14:29:08.664150911 -0400 @@ -1,7 +1,7 @@ function test() { try { console.log(typeof x); - Object.assign({}, { x: 1 }); + ({ x: 1}); let x; } catch (e) { console.log(e.name);
ReferenceError
r1-p005 · Sparse Array Hazard
1const arr = Array(3);
2arr[1] = 'a';
3const obj = Object.assign({}, arr);
4console.log(obj.length, obj[0], obj[1], obj[2]);
--- orig.js 2026-10-22 14:29:06.541162390 -0400 +++ fixed.js 2026-10-22 14:29:08.632151081 -0400 @@ -1,4 +1,4 @@ const arr = Array(3); arr[1] = 'a'; -const obj = Object.assign({}, arr); +const obj = { ...arr}; console.log(obj.length, obj[0], obj[1], obj[2]);
undefined undefined a undefined
r1-p006 · Var Hoisting Hazard
1for (var i = 0; i < 2; i++) {
2 var obj = { x: i };
3 Object.assign({}, obj);
4}
5console.log(typeof obj, obj.x);
--- orig.js 2026-10-22 14:29:06.541162390 -0400 +++ fixed.js 2026-10-22 14:29:08.621151140 -0400 @@ -1,5 +1,5 @@ for (var i = 0; i < 2; i++) { var obj = { x: i }; - Object.assign({}, obj); + ({ ...obj}); } console.log(typeof obj, obj.x);
object 1
r1-p007 · Null/Undefined Hazard
1let obj = null;
2try {
3 Object.assign({}, obj);
4} catch (e) {
5 console.log(e.name);
6}
7obj = undefined;
8try {
9 Object.assign({}, obj);
10} catch (e) {
11 console.log(e.name);
12}
--- orig.js 2026-10-22 14:29:06.542162384 -0400 +++ fixed.js 2026-10-22 14:29:08.692150762 -0400 @@ -1,12 +1,12 @@ let obj = null; try { - Object.assign({}, obj); + ({ ...obj}); } catch (e) { console.log(e.name); } obj = undefined; try { - Object.assign({}, obj); + ({ ...obj}); } catch (e) { console.log(e.name); }
r1-p008 · Class Fields Hazard
1class Base {
2 static staticVal = 10;
3 instanceVal = 20;
4}
5class Derived extends Base {
6 static {
7 Derived.staticVal = Base.staticVal * 2;
8 }
9 method() {
10 return Object.assign({}, { staticVal: Derived.staticVal, instanceVal: this.instanceVal });
11 }
12}
13const d = new Derived();
14console.log(d.method().staticVal, d.method().instanceVal);
--- orig.js 2026-10-22 14:29:09.675145581 -0400 +++ fixed.js 2026-10-22 14:29:12.865129329 -0400 @@ -7,7 +7,7 @@ Derived.staticVal = Base.staticVal * 2; } method() { - return Object.assign({}, { staticVal: Derived.staticVal, instanceVal: this.instanceVal }); + return { staticVal: Derived.staticVal, instanceVal: this.instanceVal}; } } const d = new Derived();
20 20
r2-p001 · Spread Syntax Does Not Copy Property Descriptors
1const obj = {};
2Object.defineProperty(obj, 'prop', {
3 get() { return this._value || 0; },
4 set(v) { this._value = v; },
5 enumerable: false
6});
7let result = Object.assign({}, obj);
8console.log(Object.getOwnPropertyDescriptor(result, 'prop')); // has getter/setter
9result = { ...obj };
10console.log(Object.getOwnPropertyDescriptor(result, 'prop')); // undefined
--- orig.js 2026-10-22 14:30:23.713981566 -0400 +++ fixed.js 2026-10-22 14:30:26.355983559 -0400 @@ -4,7 +4,7 @@ set(v) { this._value = v; }, enumerable: false }); -let result = Object.assign({}, obj); +let result = { ...obj}; console.log(Object.getOwnPropertyDescriptor(result, 'prop')); // has getter/setter result = { ...obj }; console.log(Object.getOwnPropertyDescriptor(result, 'prop')); // undefined
undefined undefined
r2-p002 · Numeric/String Coercion with toString/valueOf
1const obj = { valueOf() { return 42; }, toString() { return 'hello'; } };
2let sum = Object.assign({}, obj) + 8;
3console.log(sum); // 50 because valueOf called
4sum = ({ ...obj }) + 8;
5console.log(sum); // NaN because spread creates plain object, valueOf lost
--- orig.js 2026-10-22 14:30:23.714981567 -0400 +++ fixed.js 2026-10-22 14:30:28.172985224 -0400 @@ -1,5 +1,5 @@ const obj = { valueOf() { return 42; }, toString() { return 'hello'; } }; -let sum = Object.assign({}, obj) + 8; +let sum = ({ ...obj}) + 8; console.log(sum); // 50 because valueOf called sum = ({ ...obj }) + 8; console.log(sum); // NaN because spread creates plain object, valueOf lost
50 50
r2-p003 · Name Collisions Across Scopes After a Move
1function outer() {
2 const x = { a: 1 };
3 if (true) {
4 let x = { b: 2 }; // shadows outer x
5 console.log(Object.assign({}, x).a); // undefined, x is inner
6 console.log(({ ...x }).a); // undefined
7 }
8}
9outer();
--- orig.js 2026-10-22 14:30:23.715981567 -0400 +++ fixed.js 2026-10-22 14:30:28.103985154 -0400 @@ -2,7 +2,7 @@ const x = { a: 1 }; if (true) { let x = { b: 2 }; // shadows outer x - console.log(Object.assign({}, x).a); // undefined, x is inner + console.log(({ ...x}).a); // undefined, x is inner console.log(({ ...x }).a); // undefined } }
undefined undefined
r2-p004 · Function Declarations Inside Blocks/Switch/Labeled Statements
1switch (0) {
2 case 0:
3 function f() { return { y: 1 }; }
4 let r = Object.assign({}, f());
5 console.log(r.y); // 1
6 r = ({ ...f() });
7 console.log(r.y); // 1
8 break;
9}
--- orig.js 2026-10-22 14:30:23.716981568 -0400 +++ fixed.js 2026-10-22 14:30:28.157985209 -0400 @@ -1,7 +1,7 @@ switch (0) { case 0: function f() { return { y: 1 }; } - let r = Object.assign({}, f()); + let r = { ...f()}; console.log(r.y); // 1 r = ({ ...f() }); console.log(r.y); // 1
1 1
r2-p005 · Sparse Arrays, Holes, Array(n) vs [n]
1const arr = Array(3);
2arr[1] = 'a';
3console.log(Object.assign({}, arr)); // { '1': 'a' }
4console.log({ ...arr }); // { '1': 'a' }
5// Both same for enum props, but test toString
6arr.toString = () => 'custom';
7console.log(String(Object.assign({}, arr))); // [object Object]
8console.log(String({ ...arr })); // [object Object]
--- orig.js 2026-10-22 14:30:23.716981568 -0400 +++ fixed.js 2026-10-22 14:30:26.945984068 -0400 @@ -1,8 +1,8 @@ const arr = Array(3); arr[1] = 'a'; -console.log(Object.assign({}, arr)); // { '1': 'a' } +console.log({ ...arr}); // { '1': 'a' } console.log({ ...arr }); // { '1': 'a' } // Both same for enum props, but test toString arr.toString = () => 'custom'; -console.log(String(Object.assign({}, arr))); // [object Object] +console.log(String({ ...arr})); // [object Object] console.log(String({ ...arr })); // [object Object]
{ '1': 'a' }
{ '1': 'a' }
custom
customr2-p006 · Complex Expression as First Argument (Evaluation Side Effects)
1let callCount = 0;
2function getObj() {
3 callCount++;
4 return { x: callCount };
5}
6let result = Object.assign(getObj(), { y: 2 });
7console.log(callCount, result.x, result.y); // 1, 1, 2
8result = ({ ...getObj(), y: 2 });
9console.log(callCount, result.x, result.y); // 2, 2, 2 (called twice)
1 1 2 2 2 2
r2-p007 · Spread Syntax Does Not Copy Property Descriptors
1const obj = {};
2Object.defineProperty(obj, 'prop', {
3 get() { return this._value || 0; },
4 set(v) { this._value = v; },
5 enumerable: false
6});
7let result = Object.assign({}, obj);
8console.log('desc:', Object.getOwnPropertyDescriptor(result, 'prop'));
9result = { ...obj };
10console.log('desc:', Object.getOwnPropertyDescriptor(result, 'prop'));
--- orig.js 2026-10-22 14:30:28.052985103 -0400 +++ fixed.js 2026-10-22 14:30:31.378988760 -0400 @@ -4,7 +4,7 @@ set(v) { this._value = v; }, enumerable: false }); -let result = Object.assign({}, obj); +let result = { ...obj}; console.log('desc:', Object.getOwnPropertyDescriptor(result, 'prop')); result = { ...obj }; console.log('desc:', Object.getOwnPropertyDescriptor(result, 'prop'));
desc: undefined desc: undefined
r2-p008 · Numeric/String Coercion with toString/valueOf
1const obj = { valueOf() { return 42; }, toString() { return 'hello'; } };
2let sum = Object.assign({}, obj) + 8;
3console.log('sum:', sum);
4sum = ({ ...obj }) + 8;
5console.log('sum:', sum);
--- orig.js 2026-10-22 14:30:28.144985196 -0400 +++ fixed.js 2026-10-22 14:30:31.378988760 -0400 @@ -1,5 +1,5 @@ const obj = { valueOf() { return 42; }, toString() { return 'hello'; } }; -let sum = Object.assign({}, obj) + 8; +let sum = ({ ...obj}) + 8; console.log('sum:', sum); sum = ({ ...obj }) + 8; console.log('sum:', sum);
sum: 50 sum: 50
r2-p009 · Name Collisions Across Scopes After a Move
1function outer() {
2 const x = { a: 1 };
3 if (true) {
4 let x = { b: 2 };
5 console.log('outer.x.a:', Object.assign({}, x).a);
6 console.log('outer.x.a:', ({ ...x }).a);
7 }
8}
9outer();
--- orig.js 2026-10-22 14:30:29.724986842 -0400 +++ fixed.js 2026-10-22 14:30:32.995990817 -0400 @@ -2,7 +2,7 @@ const x = { a: 1 }; if (true) { let x = { b: 2 }; - console.log('outer.x.a:', Object.assign({}, x).a); + console.log('outer.x.a:', ({ ...x}).a); console.log('outer.x.a:', ({ ...x }).a); } }
outer.x.a: undefined outer.x.a: undefined
r2-p010 · Function Declarations Inside Blocks/Switch/Labeled Statements
1switch (0) {
2 case 0:
3 function f() { return { y: 1 }; }
4 let r = Object.assign({}, f());
5 console.log('r.y:', r.y);
6 r = ({ ...f() });
7 console.log('r.y:', r.y);
8 break;
9}
--- orig.js 2026-10-22 14:30:29.796986920 -0400 +++ fixed.js 2026-10-22 14:30:32.417990065 -0400 @@ -1,7 +1,7 @@ switch (0) { case 0: function f() { return { y: 1 }; } - let r = Object.assign({}, f()); + let r = { ...f()}; console.log('r.y:', r.y); r = ({ ...f() }); console.log('r.y:', r.y);
r.y: 1 r.y: 1
r2-p011 · Sparse Arrays, Holes, Array(n) vs [n]
1const arr = Array(3);
2arr[1] = 'a';
3console.log('assigned:', Object.assign({}, arr));
4console.log('spread:', { ...arr });
5arr.toString = () => 'custom';
6console.log('String assigned:', String(Object.assign({}, arr)));
7console.log('String spread:', String({ ...arr }));
--- orig.js 2026-10-22 14:30:29.796986920 -0400 +++ fixed.js 2026-10-22 14:30:32.414990062 -0400 @@ -1,7 +1,7 @@ const arr = Array(3); arr[1] = 'a'; -console.log('assigned:', Object.assign({}, arr)); +console.log('assigned:', { ...arr}); console.log('spread:', { ...arr }); arr.toString = () => 'custom'; -console.log('String assigned:', String(Object.assign({}, arr))); +console.log('String assigned:', String({ ...arr})); console.log('String spread:', String({ ...arr }));
assigned: { '1': 'a' }
spread: { '1': 'a' }
String assigned: custom
String spread: customr2-p012 · Complex Expression as First Argument (Evaluation Order Side Effects)
1let state = 0;
2function getObj() {
3 state++;
4 return { x: state };
5}
6let result = Object.assign({x: state}, {y: state}, getObj());
7console.log('state:', state, 'result:', result);
8result = ({x: state, y: state, ...getObj()});
9console.log('state:', state, 'result:', result);
--- orig.js 2026-10-22 14:30:30.199987371 -0400 +++ fixed.js 2026-10-22 14:30:34.486992899 -0400 @@ -3,7 +3,7 @@ state++; return { x: state }; } -let result = Object.assign({x: state}, {y: state}, getObj()); +let result = {x: state, y: state, ...getObj()}; console.log('state:', state, 'result:', result); result = ({x: state, y: state, ...getObj()}); console.log('state:', state, 'result:', result);
state: 1 result: { x: 1, y: 0 }
state: 2 result: { x: 2, y: 1 }r2-p013 · Precedence/Grouping in Larger Expressions
1let x = {a: 1};
2let y = Object.assign({}, x) + 2;
3console.log('y:', y);
4y = ({ ...x }) + 2;
5console.log('y:', y);
--- orig.js 2026-10-22 14:30:33.015990844 -0400 +++ fixed.js 2026-10-22 14:30:36.226995518 -0400 @@ -1,5 +1,5 @@ let x = {a: 1}; -let y = Object.assign({}, x) + 2; +let y = ({ ...x}) + 2; console.log('y:', y); y = ({ ...x }) + 2; console.log('y:', y);
y: [object Object]2 y: [object Object]2
r2-p014 · Class Fields, Static Blocks, Private Names, Super Calls (generated program does not parse: /home/sun/dev/diaghunt/runs/2026-10-22T18-28-5)
1class Base {
2 constructor() { this.public = 1; }
3}
4class Derived extends Base {
5 #private = 2;
6 static staticField = 3;
7 constructor() {
8 super();
9 console.log('super.public:', Object.assign({}, super).public);
10 console.log('super.public:', ({ ...super }).public);
11 }
12}
13new Derived();