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!
};