Looking At: no-misused-spread

@typescript-eslint recently added a new rule no-misused-spread. This prevents accidental use of the spread operator in unintended ways. From the documentation:

  • Spreading a Promise into an object. You probably meant to await it.
  • Spreading a function without properties into an object. You probably meant to call it.
  • Spreading an iterable (Array, Map, etc.) into an object. Iterable objects usually do not have meaningful enumerable properties and you probably meant to spread it into an array instead.
  • Spreading a string into an array. String enumeration behaviors in JavaScript around encoded characters are often surprising.
  • Spreading a class into an object. This copies all static own properties of the class, but none of the inheritance chain.
  • Spreading a class instance into an object. This does not faithfully copy the instance because only its own properties are copied, but the inheritance chain is lost, including all its methods.

Before & After

Let's look at the before and after of incorrect usages of the spread, and the correct usage.

By spreading a promise, nothing will be added to the final object:

const simplePromise = async () => {
  return {value: 1};
};
const object = {
  name: "badPromise",
  ...simplePromise,
};
// { name: "badPromise" }
const object = {
  name: "goodPromise",
  ...(await simplePromise()),
};
// { name: "goodPromise", value: 1 }

Interestingly in the function case, TypeScript seems able to check if the function has properties on it. At least within the same scope:

const withoutProperties = () => {
  return "hello";
};
const withProperties = () => {
  return "hello";
};
withProperties.count = 1;
const object = {
  ...withProperties, // No Error
  ...withoutProperties, // Error!
};
// { count: 1 }

For spreading an iterable, this is likely a mistake. The intention is more likely that you want to spread it into a new array or assign its value.

const list = [1, 2, 3];
const map = new Map([["a", 1], ["b", 2], ["c", 3]]);
const set = new Set([1, 2, 3]);
const object = {
  list, // No Error
  list2: [...list], // No Error
  map: [...map], // No Error
  set: [...set], // No Error
  ...list, // Error!
};

For strings, the rule will point out the likely mistake when you try to spread into an array.

const message = "Hello";
const object = {
  message, // No Error
  message2: split(message, ""), // No Error (Explicit intention)
  message2: [...message], // Error!
};

When you spread a class, you will only get static properties, not even static methods.

class MyClass {
  public static readonly hello = "hello";
  public readonly world = "world";
  public static getHello() {
    return MyClass.hello;
  }
  public getWorld() {
    return this.world;
  }
}
const object = {
  ...MyClass, // Error!
};
// { hello: "hello"}

And of course, the same issue happens when you spread the instance of a class. This, in my opinion, very important as with third-party libraries, we don't always know if an object is a plain object or a class instance.

const object = {
  ...(new MyClass()), // Error!
};