oxc/no-map-spread

Disallow object and array spreads in the return value of map and flatMap callbacks

has suggestions

tasks.map(task => ({ ...task, done: false })) copies every property of every item into a new object. On a large array, that's a lot of work and memory. If nothing else uses the old items, you can change them in place.

The rule looks at the object or array the callback returns, directly or through a variable. It doesn't look at what's being spread, so ids.map(id => ({ ...defaults, id })) is reported too.

--fix-suggestions rewrites { ...task, done: false } to Object.assign(task, { done: false }). That changes the original array's items.

Configure#

Off by default. To turn it on:

bunfig.toml
[lint.rules]
"oxc/no-map-spread" = "error" # or "warn"

Every option, set to its default:

bunfig.toml
[lint.rules]
"oxc/no-map-spread" = ["error", {
  ignoreRereads = true,
  ignoreArgs = true,
}]

Examples#

example.js1 problem
function loadTasks() {
  const tasks = readTasks();
  return tasks.map(task => ({ ...task, done: false }));
}
example.js1 problem
const rows = readRows().map(row => [...row, total(row)]);
example.jsNo problems
function loadTasks() {
  const tasks = readTasks();
  return tasks.map(task => Object.assign(task, { done: false }));
}
example.jsNo problems
const rows = readRows();
for (const row of rows) {
  row.push(total(row));
}

An array that belongs to this is never reported, because changing its items would change the instance:

example.jsNo problems
class TaskList {
  snapshot() {
    return this.tasks.map(task => ({ ...task }));
  }
}

Options#

OptionDefaultWhat it does
ignoreRereadstrueAllow the spread if the array variable is read again after the call. In that case, the copy is probably on purpose.
ignoreArgstrueAllow the spread if the array is a function parameter. The caller might still use its items.

Allowed by default, because tasks is used afterwards:

example.jsNo problems
const tasks = readTasks();
const drafts = tasks.map(task => ({ ...task, done: false }));
console.log(tasks.length, drafts.length);
example.jswith { "ignoreRereads": false }1 problem
const tasks = readTasks();
const drafts = tasks.map(task => ({ ...task, done: false }));
console.log(tasks.length, drafts.length);

Allowed by default, because tasks is a parameter:

example.jsNo problems
function markDone(tasks) {
  return tasks.map(task => ({ ...task, done: true }));
}
example.jswith { "ignoreArgs": false }1 problem
function markDone(tasks) {
  return tasks.map(task => ({ ...task, done: true }));
}

When not to use it#

If your code treats data as immutable, as React state and Redux stores require, the copy is the point. The cost also doesn't matter for small arrays.