diff --git a/packages/eslint-plugin/docs/rules/no-misused-spread.mdx b/packages/eslint-plugin/docs/rules/no-misused-spread.mdx
new file mode 100644
index 000000000000..17ba58cc6aff
--- /dev/null
+++ b/packages/eslint-plugin/docs/rules/no-misused-spread.mdx
@@ -0,0 +1,141 @@
+---
+description: 'Disallow using the spread operator when it might cause unexpected behavior.'
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+> 🛑 This file is source code, not the primary documentation location! 🛑
+>
+> See **https://typescript-eslint.io/rules/no-misused-spread** for documentation.
+
+The [spread operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax) (`...`) is a JavaScript
+feature that can be misused in ways not always reported by TypeScript. This rule disallows using the spread operator on types where
+spreading can lead to unexpected behavior.
+
+This rule disallows the spread operator in the following cases:
+
+- Spreading a string into an array
+- Spreading a `Promise` into an object
+- Spreading a function without properties into an object
+- Spreading a class instance or declaration into an object
+- Spreading an iterable (`Map`, `Array`, etc.) into an object
+
+## Examples
+
+
+
+
+```ts
+declare const userName: string;
+const chars = [...userName];
+
+declare const arr: number[];
+const arrSpread = { ...arr };
+
+declare const set: Set;
+const setSpread = { ...set };
+
+declare const map: Map;
+const mapSpread = { ...map };
+
+declare function getObj(): { a: 1; b: 2 };
+const getObjSpread = { ...getObj };
+```
+
+
+
+
+```ts
+declare const userName: string;
+const chars = userName.split('');
+
+declare const arr: number[];
+const arrSpread = [...arr];
+
+declare const set: Set;
+const setSpread = [...set];
+
+declare const map: Map;
+const mapObject = Object.fromEntries(map);
+
+declare function getObj(): { a: 1; b: 2 };
+const getObjSpread = { ...getObj() };
+```
+
+
+
+
+## Options
+
+### `allow`
+
+This option allows marking specific types as "safe" to be spread. It takes an
+array of type specifiers to consider safe.
+
+This option takes the shared [`TypeOrValueSpecifier` format](/packages/type-utils/type-or-value-specifier).
+
+Examples of a configuration for this option:
+
+```json
+"@typescript-eslint/no-misused-spread": [
+ "error",
+ {
+ "allowForKnownSafeIterables": [
+ "SafeType",
+ { "from": "file", "name": "SafeString", "path": "src/safe-string.ts" },
+ { "from": "lib", "name": "BrandedArray" },
+ { "from": "package", "name": "ThisIsSafe", "package": "safe-lib" }
+ ]
+ }
+]
+```
+
+
+
+
+```ts
+type UnsafeIterable = Iterable;
+
+declare const iterable: UnsafeIterable;
+
+const spreadIterable = { ...iterable };
+
+type UnsafeBrandedString = string & { __brand: 'unsafe' };
+
+declare const brandedString: UnsafeBrandedString;
+
+const spreadBrandedString = { ...brandedString };
+```
+
+
+
+
+```ts option='{"allow":["SafeIterable", "BrandedString"]}'
+type SafeIterable = Iterable;
+
+declare const iterable: SafeIterable;
+
+const spreadIterable = { ...iterable };
+
+type BrandedString = string & { __brand: 'safe' };
+
+declare const brandedString: BrandedString;
+
+const spreadBrandedString = { ...brandedString };
+```
+
+
+
+
+## When Not To Use It
+
+If you intentionally want to use the spread operator in those cases, and expect
+the specific behavior that comes with it, you might not want this rule.
+For example, when you want to spread an array into an object and expect the
+result to be an object with the array elements as values and the array indices
+as keys.
+
+If your use cases for unusual spreads only involve a few types, you might consider using
+[ESLint disable comments](https://eslint.org/docs/latest/use/configure/rules#using-configuration-comments-1)
+and/or the [`allow` option](#allow) instead of completely disabling this rule.
diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts
index 107f369260ec..ca8c86837e4f 100644
--- a/packages/eslint-plugin/src/configs/all.ts
+++ b/packages/eslint-plugin/src/configs/all.ts
@@ -74,6 +74,7 @@ export = {
'@typescript-eslint/no-meaningless-void-operator': 'error',
'@typescript-eslint/no-misused-new': 'error',
'@typescript-eslint/no-misused-promises': 'error',
+ '@typescript-eslint/no-misused-spread': 'error',
'@typescript-eslint/no-mixed-enums': 'error',
'@typescript-eslint/no-namespace': 'error',
'@typescript-eslint/no-non-null-asserted-nullish-coalescing': 'error',
diff --git a/packages/eslint-plugin/src/configs/disable-type-checked.ts b/packages/eslint-plugin/src/configs/disable-type-checked.ts
index 7cf867b382f2..2992c793b0ed 100644
--- a/packages/eslint-plugin/src/configs/disable-type-checked.ts
+++ b/packages/eslint-plugin/src/configs/disable-type-checked.ts
@@ -25,6 +25,7 @@ export = {
'@typescript-eslint/no-implied-eval': 'off',
'@typescript-eslint/no-meaningless-void-operator': 'off',
'@typescript-eslint/no-misused-promises': 'off',
+ '@typescript-eslint/no-misused-spread': 'off',
'@typescript-eslint/no-mixed-enums': 'off',
'@typescript-eslint/no-redundant-type-constituents': 'off',
'@typescript-eslint/no-unnecessary-boolean-literal-compare': 'off',
diff --git a/packages/eslint-plugin/src/configs/strict-type-checked-only.ts b/packages/eslint-plugin/src/configs/strict-type-checked-only.ts
index 8bfba2276c9d..49b4fc5e5e22 100644
--- a/packages/eslint-plugin/src/configs/strict-type-checked-only.ts
+++ b/packages/eslint-plugin/src/configs/strict-type-checked-only.ts
@@ -22,6 +22,7 @@ export = {
'@typescript-eslint/no-implied-eval': 'error',
'@typescript-eslint/no-meaningless-void-operator': 'error',
'@typescript-eslint/no-misused-promises': 'error',
+ '@typescript-eslint/no-misused-spread': 'error',
'@typescript-eslint/no-mixed-enums': 'error',
'@typescript-eslint/no-redundant-type-constituents': 'error',
'@typescript-eslint/no-unnecessary-boolean-literal-compare': 'error',
diff --git a/packages/eslint-plugin/src/configs/strict-type-checked.ts b/packages/eslint-plugin/src/configs/strict-type-checked.ts
index 4a5c7adfc93e..922811130fdb 100644
--- a/packages/eslint-plugin/src/configs/strict-type-checked.ts
+++ b/packages/eslint-plugin/src/configs/strict-type-checked.ts
@@ -36,6 +36,7 @@ export = {
'@typescript-eslint/no-meaningless-void-operator': 'error',
'@typescript-eslint/no-misused-new': 'error',
'@typescript-eslint/no-misused-promises': 'error',
+ '@typescript-eslint/no-misused-spread': 'error',
'@typescript-eslint/no-mixed-enums': 'error',
'@typescript-eslint/no-namespace': 'error',
'@typescript-eslint/no-non-null-asserted-nullish-coalescing': 'error',
diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts
index 49d8bd67c5cb..87c316bba5f0 100644
--- a/packages/eslint-plugin/src/rules/index.ts
+++ b/packages/eslint-plugin/src/rules/index.ts
@@ -53,6 +53,7 @@ import noMagicNumbers from './no-magic-numbers';
import noMeaninglessVoidOperator from './no-meaningless-void-operator';
import noMisusedNew from './no-misused-new';
import noMisusedPromises from './no-misused-promises';
+import noMisusedSpread from './no-misused-spread';
import noMixedEnums from './no-mixed-enums';
import noNamespace from './no-namespace';
import noNonNullAssertedNullishCoalescing from './no-non-null-asserted-nullish-coalescing';
@@ -182,6 +183,7 @@ export default {
'no-meaningless-void-operator': noMeaninglessVoidOperator,
'no-misused-new': noMisusedNew,
'no-misused-promises': noMisusedPromises,
+ 'no-misused-spread': noMisusedSpread,
'no-mixed-enums': noMixedEnums,
'no-namespace': noNamespace,
'no-non-null-asserted-nullish-coalescing': noNonNullAssertedNullishCoalescing,
diff --git a/packages/eslint-plugin/src/rules/no-misused-spread.ts b/packages/eslint-plugin/src/rules/no-misused-spread.ts
new file mode 100644
index 000000000000..5c4ffa8a409f
--- /dev/null
+++ b/packages/eslint-plugin/src/rules/no-misused-spread.ts
@@ -0,0 +1,292 @@
+import type { TSESTree } from '@typescript-eslint/utils';
+import * as tsutils from 'ts-api-utils';
+import * as ts from 'typescript';
+
+import type { TypeOrValueSpecifier } from '../util';
+import {
+ createRule,
+ getConstrainedTypeAtLocation,
+ getParserServices,
+ isBuiltinSymbolLike,
+ isPromiseLike,
+ isTypeFlagSet,
+ readonlynessOptionsSchema,
+ typeMatchesSpecifier,
+} from '../util';
+
+type Options = [
+ {
+ allow?: TypeOrValueSpecifier[];
+ },
+];
+
+type MessageIds =
+ | 'noStringSpreadInArray'
+ | 'noPromiseSpreadInObject'
+ | 'noIterableSpreadInObject'
+ | 'noFunctionSpreadInObject'
+ | 'noMapSpreadInObject'
+ | 'noArraySpreadInObject'
+ | 'noClassInstanceSpreadInObject'
+ | 'noClassDeclarationSpreadInObject';
+
+export default createRule({
+ name: 'no-misused-spread',
+ meta: {
+ type: 'problem',
+ docs: {
+ description:
+ 'Disallow using the spread operator when it might cause unexpected behavior',
+ recommended: 'strict',
+ requiresTypeChecking: true,
+ },
+ messages: {
+ noStringSpreadInArray:
+ "Using the spread operator on a string can cause unexpected behavior. Prefer `String.split('')` instead.",
+
+ noPromiseSpreadInObject:
+ 'Using the spread operator on Promise in an object can cause unexpected behavior. Did you forget to await the promise?',
+
+ noIterableSpreadInObject:
+ 'Using the spread operator on an Iterable in an object can cause unexpected behavior.',
+
+ noFunctionSpreadInObject:
+ 'Using the spread operator on a function without additional properties can cause unexpected behavior. Did you forget to call the function?',
+
+ noMapSpreadInObject:
+ 'Using the spread operator on a Map in an object will result in an emtpy object. Did you mean to use `Object.fromEntries(map)` instead?',
+
+ noArraySpreadInObject:
+ 'Using the spread operator on an array in an object will result in a list of indices.',
+
+ noClassInstanceSpreadInObject:
+ 'Using the spread operator on class instances will lose their class prototype.',
+
+ noClassDeclarationSpreadInObject:
+ 'Using the spread operator on class declarations will spread only their static properties, and will lose their class prototype.',
+ },
+ schema: [
+ {
+ type: 'object',
+ properties: {
+ allow: {
+ ...readonlynessOptionsSchema.properties.allow,
+ description:
+ 'An array of type specifiers that are known to be safe to spread.',
+ },
+ },
+ additionalProperties: false,
+ },
+ ],
+ },
+
+ defaultOptions: [
+ {
+ allow: [],
+ },
+ ],
+
+ create(context, [options]) {
+ const services = getParserServices(context);
+ const checker = services.program.getTypeChecker();
+
+ function checkArraySpread(node: TSESTree.SpreadElement): void {
+ const type = getConstrainedTypeAtLocation(services, node.argument);
+
+ if (isTypeAllowed(type)) {
+ return;
+ }
+
+ if (isString(type)) {
+ context.report({
+ node,
+ messageId: 'noStringSpreadInArray',
+ });
+
+ return;
+ }
+ }
+
+ function checkObjectSpread(node: TSESTree.SpreadElement): void {
+ const type = getConstrainedTypeAtLocation(services, node.argument);
+
+ if (isTypeAllowed(type)) {
+ return;
+ }
+
+ if (isPromise(services.program, type)) {
+ context.report({
+ node,
+ messageId: 'noPromiseSpreadInObject',
+ });
+
+ return;
+ }
+
+ if (isFunctionWithoutProps(type)) {
+ context.report({
+ node,
+ messageId: 'noFunctionSpreadInObject',
+ });
+
+ return;
+ }
+
+ if (isMap(services.program, type)) {
+ context.report({
+ node,
+ messageId: 'noMapSpreadInObject',
+ });
+
+ return;
+ }
+
+ if (isArray(checker, type)) {
+ context.report({
+ node,
+ messageId: 'noArraySpreadInObject',
+ });
+
+ return;
+ }
+
+ if (
+ isIterable(type, checker) &&
+ // Don't report when the type is string, since TS will flag it already
+ !isString(type)
+ ) {
+ context.report({
+ node,
+ messageId: 'noIterableSpreadInObject',
+ });
+
+ return;
+ }
+
+ if (isClassInstance(services.program, type)) {
+ context.report({
+ node,
+ messageId: 'noClassInstanceSpreadInObject',
+ });
+
+ return;
+ }
+
+ if (isClassDeclaration(type)) {
+ context.report({
+ node,
+ messageId: 'noClassDeclarationSpreadInObject',
+ });
+
+ return;
+ }
+ }
+
+ function isTypeAllowed(type: ts.Type): boolean {
+ if (!options.allow) {
+ return false;
+ }
+
+ return options.allow.some(specifier =>
+ typeMatchesSpecifier(type, specifier, services.program),
+ );
+ }
+
+ return {
+ 'ArrayExpression > SpreadElement': checkArraySpread,
+ 'ObjectExpression > SpreadElement': checkObjectSpread,
+ };
+ },
+});
+
+function isIterable(type: ts.Type, checker: ts.TypeChecker): boolean {
+ return tsutils
+ .typeParts(type)
+ .some(
+ t => !!tsutils.getWellKnownSymbolPropertyOfType(t, 'iterator', checker),
+ );
+}
+
+function isArray(checker: ts.TypeChecker, type: ts.Type): boolean {
+ return isTypeRecurser(
+ type,
+ t => checker.isArrayType(t) || checker.isTupleType(t),
+ );
+}
+
+function isString(type: ts.Type): boolean {
+ return isTypeRecurser(type, t => isTypeFlagSet(t, ts.TypeFlags.StringLike));
+}
+
+function isFunctionWithoutProps(type: ts.Type): boolean {
+ return isTypeRecurser(
+ type,
+ t => t.getCallSignatures().length > 0 && t.getProperties().length === 0,
+ );
+}
+
+function isPromise(program: ts.Program, type: ts.Type): boolean {
+ return isTypeRecurser(type, t => isPromiseLike(program, t));
+}
+
+// Builtin classes that are known to be problematic when spread,
+// but can't be detected in a reliable way.
+const BUILTIN_CLASSES = ['WeakRef'];
+
+function isClassInstance(program: ts.Program, type: ts.Type): boolean {
+ return isTypeRecurser(type, t => {
+ const symbol = t.getSymbol();
+
+ if (!symbol) {
+ return false;
+ }
+
+ const isBuiltinProblematic = BUILTIN_CLASSES.some(name =>
+ isBuiltinSymbolLike(program, t, name),
+ );
+
+ if (isBuiltinProblematic) {
+ return true;
+ }
+
+ return (
+ t.isClassOrInterface() &&
+ tsutils.isSymbolFlagSet(t.symbol, ts.SymbolFlags.Value)
+ );
+ });
+}
+
+function isClassDeclaration(type: ts.Type): boolean {
+ return isTypeRecurser(type, t => {
+ if (
+ tsutils.isObjectType(t) &&
+ tsutils.isObjectFlagSet(t, ts.ObjectFlags.InstantiationExpressionType)
+ ) {
+ return true;
+ }
+
+ const kind = t.getSymbol()?.valueDeclaration?.kind;
+
+ return (
+ kind === ts.SyntaxKind.ClassDeclaration ||
+ kind === ts.SyntaxKind.ClassExpression
+ );
+ });
+}
+
+function isMap(program: ts.Program, type: ts.Type): boolean {
+ return isTypeRecurser(type, t =>
+ isBuiltinSymbolLike(program, t, ['Map', 'ReadonlyMap', 'WeakMap']),
+ );
+}
+
+function isTypeRecurser(
+ type: ts.Type,
+ predicate: (t: ts.Type) => boolean,
+): boolean {
+ if (type.isUnionOrIntersection()) {
+ return type.types.some(t => isTypeRecurser(t, predicate));
+ }
+
+ return predicate(type);
+}
diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-misused-spread.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-misused-spread.shot
new file mode 100644
index 000000000000..f18a788fffbf
--- /dev/null
+++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-misused-spread.shot
@@ -0,0 +1,82 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Validating rule docs no-misused-spread.mdx code examples ESLint output 1`] = `
+"Incorrect
+
+declare const userName: string;
+const chars = [...userName];
+ ~~~~~~~~~~~ Using the spread operator on a string can cause unexpected behavior. Prefer \`String.split('')\` instead.
+
+declare const arr: number[];
+const arrSpread = { ...arr };
+ ~~~~~~ Using the spread operator on an array in an object will result in a list of indices.
+
+declare const set: Set;
+const setSpread = { ...set };
+ ~~~~~~ Using the spread operator on an Iterable in an object can cause unexpected behavior.
+
+declare const map: Map;
+const mapSpread = { ...map };
+ ~~~~~~ Using the spread operator on a Map in an object will result in an emtpy object. Did you mean to use \`Object.fromEntries(map)\` instead?
+
+declare function getObj(): { a: 1; b: 2 };
+const getObjSpread = { ...getObj };
+ ~~~~~~~~~ Using the spread operator on a function without additional properties can cause unexpected behavior. Did you forget to call the function?
+"
+`;
+
+exports[`Validating rule docs no-misused-spread.mdx code examples ESLint output 2`] = `
+"Correct
+
+declare const userName: string;
+const chars = userName.split('');
+
+declare const arr: number[];
+const arrSpread = [...arr];
+
+declare const set: Set;
+const setSpread = [...set];
+
+declare const map: Map;
+const mapObject = Object.fromEntries(map);
+
+declare function getObj(): { a: 1; b: 2 };
+const getObjSpread = { ...getObj() };
+"
+`;
+
+exports[`Validating rule docs no-misused-spread.mdx code examples ESLint output 3`] = `
+"Incorrect
+
+type UnsafeIterable = Iterable;
+
+declare const iterable: UnsafeIterable;
+
+const spreadIterable = { ...iterable };
+ ~~~~~~~~~~~ Using the spread operator on an Iterable in an object can cause unexpected behavior.
+
+type UnsafeBrandedString = string & { __brand: 'unsafe' };
+
+declare const brandedString: UnsafeBrandedString;
+
+const spreadBrandedString = { ...brandedString };
+"
+`;
+
+exports[`Validating rule docs no-misused-spread.mdx code examples ESLint output 4`] = `
+"Correct
+Options: {"allow":["SafeIterable", "BrandedString"]}
+
+type SafeIterable = Iterable;
+
+declare const iterable: SafeIterable;
+
+const spreadIterable = { ...iterable };
+
+type BrandedString = string & { __brand: 'safe' };
+
+declare const brandedString: BrandedString;
+
+const spreadBrandedString = { ...brandedString };
+"
+`;
diff --git a/packages/eslint-plugin/tests/fixtures/tsconfig.json b/packages/eslint-plugin/tests/fixtures/tsconfig.json
index a0fc993b1f48..827ba1ff3132 100644
--- a/packages/eslint-plugin/tests/fixtures/tsconfig.json
+++ b/packages/eslint-plugin/tests/fixtures/tsconfig.json
@@ -5,7 +5,7 @@
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
- "lib": ["es2015", "es2017", "esnext"],
+ "lib": ["es2015", "es2017", "esnext", "DOM"],
"experimentalDecorators": true
},
"include": [
diff --git a/packages/eslint-plugin/tests/rules/no-misused-spread.test.ts b/packages/eslint-plugin/tests/rules/no-misused-spread.test.ts
new file mode 100644
index 000000000000..2ca7c6df93f4
--- /dev/null
+++ b/packages/eslint-plugin/tests/rules/no-misused-spread.test.ts
@@ -0,0 +1,1448 @@
+import { noFormat, RuleTester } from '@typescript-eslint/rule-tester';
+
+import rule from '../../src/rules/no-misused-spread';
+import { getFixturesRootDir } from '../RuleTester';
+
+const rootPath = getFixturesRootDir();
+
+const ruleTester = new RuleTester({
+ languageOptions: {
+ parserOptions: {
+ tsconfigRootDir: rootPath,
+ project: './tsconfig.json',
+ },
+ },
+});
+
+ruleTester.run('no-misused-spread', rule, {
+ valid: [
+ 'const a = [...[1, 2, 3]];',
+ 'const a = [...([1, 2, 3] as const)];',
+
+ `
+ declare const data: any;
+ const a = [...data];
+ `,
+
+ `
+ declare const data: number[] | any;
+ const a = [...data];
+ `,
+
+ `
+ declare const data: number[] & any;
+ const a = [...data];
+ `,
+
+ `
+ const a = [1, 2, 3];
+ const b = [...a];
+ `,
+
+ `
+ const a = [1, 2, 3] as const;
+ const b = [...a];
+ `,
+
+ `
+ declare function getArray(): number[];
+ const a = [...getArray()];
+ `,
+
+ `
+ declare function getTuple(): readonly number[];
+ const a = [...getTuple()];
+ `,
+
+ `
+ const iterator = {
+ *[Symbol.iterator]() {
+ yield 1;
+ yield 2;
+ yield 3;
+ },
+ };
+
+ const a = [...iterator];
+ `,
+
+ `
+ declare const data: Iterable | number[];
+
+ const a = [...data];
+ `,
+
+ `
+ declare const data: Iterable & number[];
+
+ const a = [...data];
+ `,
+
+ `
+ declare function getIterable(): Iterable;
+
+ const a = [...getIterable()];
+ `,
+
+ `
+ declare const data: Uint8Array;
+
+ const a = [...data];
+ `,
+
+ `
+ declare const data: TypedArray;
+
+ const a = [...data];
+ `,
+
+ 'const o = { ...{ a: 1, b: 2 } };',
+
+ 'const o = { ...({ a: 1, b: 2 } as const) };',
+
+ `
+ declare const obj: any;
+
+ const o = { ...obj };
+ `,
+
+ `
+ declare const obj: { a: number; b: number } | any;
+
+ const o = { ...obj };
+ `,
+
+ `
+ declare const obj: { a: number; b: number } & any;
+
+ const o = { ...obj };
+ `,
+
+ `
+ const obj = { a: 1, b: 2 };
+ const o = { ...obj };
+ `,
+
+ `
+ declare const obj: { a: number; b: number };
+ const o = { ...obj };
+ `,
+
+ `
+ declare function getObject(): { a: number; b: number };
+ const o = { ...getObject() };
+ `,
+
+ `
+ function f() {}
+
+ f.prop = 1;
+
+ const o = { ...f };
+ `,
+
+ `
+ const f = () => {};
+
+ f.prop = 1;
+
+ const o = { ...f };
+ `,
+
+ `
+ function* generator() {}
+
+ generator.prop = 1;
+
+ const o = { ...generator };
+ `,
+
+ `
+ declare const promiseLike: PromiseLike;
+
+ const o = { ...promiseLike };
+ `,
+
+ {
+ options: [{ allow: ['Promise'] }],
+ code: `
+ const promise = new Promise(() => {});
+ const o = { ...promise };
+ `,
+ },
+
+ `
+ interface A {}
+
+ declare const a: A;
+
+ const o = { ...a };
+ `,
+
+ // This case is being flagged by TS already, but since we check in the code
+ // for `Iterable`s, it catches string as well, so this test exists to ensure
+ // we don't flag it.
+ `
+ const o = { ...'test' };
+ `,
+
+ {
+ options: [{ allow: ['string'] }],
+ code: `
+ const str: string = 'test';
+ const a = [...str];
+ `,
+ },
+
+ {
+ options: [{ allow: ['f'] }],
+ code: `
+ function f() {}
+
+ const a = { ...f };
+ `,
+ },
+
+ {
+ options: [
+ {
+ allow: [{ from: 'lib', name: 'Iterable' }],
+ },
+ ],
+ code: `
+ declare const iterator: Iterable;
+
+ const a = { ...iterator };
+ `,
+ },
+
+ {
+ options: [{ allow: ['CustomIterable'] }],
+ code: `
+ type CustomIterable = {
+ [Symbol.iterator]: () => Generator;
+ };
+
+ declare const iterator: CustomIterable;
+
+ const a = { ...iterator };
+ `,
+ },
+
+ {
+ options: [
+ {
+ allow: [{ from: 'file', name: 'CustomIterable' }],
+ },
+ ],
+ code: `
+ type CustomIterable = {
+ [Symbol.iterator]: () => string;
+ };
+
+ declare const iterator: CustomIterable;
+
+ const a = { ...iterator };
+ `,
+ },
+
+ {
+ options: [
+ {
+ allow: [
+ { from: 'package', package: 'module', name: 'CustomIterable' },
+ ],
+ },
+ ],
+ code: `
+ declare module 'module' {
+ export type CustomIterable = {
+ [Symbol.iterator]: () => string;
+ };
+ }
+
+ import { CustomIterable } from 'module';
+
+ declare const iterator: CustomIterable;
+
+ const a = { ...iterator };
+ `,
+ },
+
+ {
+ options: [{ allow: ['A'] }],
+ code: `
+ class A {
+ a = 1;
+ }
+
+ const a = new A();
+
+ const o = { ...a };
+ `,
+ },
+
+ {
+ options: [{ allow: ['A'] }],
+ code: `
+ const a = {
+ ...class A {
+ static value = 1;
+ },
+ };
+ `,
+ },
+
+ // WeakSet is not iterable
+ `
+ declare const set: WeakSet;
+ const o = { ...set };
+ `,
+ ],
+
+ invalid: [
+ {
+ code: "const a = [...'test'];",
+ errors: [
+ {
+ messageId: 'noStringSpreadInArray',
+ line: 1,
+ column: 12,
+ endColumn: 21,
+ },
+ ],
+ },
+
+ {
+ code: `
+ const test = 'hello';
+ const a = [...test];
+ `,
+ errors: [
+ {
+ messageId: 'noStringSpreadInArray',
+ line: 3,
+ column: 20,
+ endColumn: 27,
+ },
+ ],
+ },
+
+ {
+ code: `
+ const test = \`he\${'ll'}o\`;
+ const a = [...test];
+ `,
+ errors: [
+ {
+ messageId: 'noStringSpreadInArray',
+ line: 3,
+ column: 20,
+ endColumn: 27,
+ },
+ ],
+ },
+
+ {
+ code: `
+ declare const test: string;
+ const a = [...test];
+ `,
+ errors: [
+ {
+ messageId: 'noStringSpreadInArray',
+ line: 3,
+ column: 20,
+ endColumn: 27,
+ },
+ ],
+ },
+
+ {
+ code: `
+ declare const test: string | number[];
+ const a = [...test];
+ `,
+ errors: [
+ {
+ messageId: 'noStringSpreadInArray',
+ line: 3,
+ column: 20,
+ endColumn: 27,
+ },
+ ],
+ },
+
+ {
+ code: `
+ declare const test: string & { __brand: 'test' };
+ const a = [...test];
+ `,
+ errors: [
+ {
+ messageId: 'noStringSpreadInArray',
+ line: 3,
+ column: 20,
+ endColumn: 27,
+ },
+ ],
+ },
+
+ {
+ code: `
+ declare const test: number | (boolean | (string & { __brand: true }));
+ const a = [...test];
+ `,
+ errors: [
+ {
+ messageId: 'noStringSpreadInArray',
+ line: 3,
+ column: 20,
+ endColumn: 27,
+ },
+ ],
+ },
+
+ {
+ code: `
+ declare function getString(): string;
+ const a = [...getString()];
+ `,
+ errors: [
+ {
+ messageId: 'noStringSpreadInArray',
+ line: 3,
+ column: 20,
+ endColumn: 34,
+ },
+ ],
+ },
+
+ {
+ code: `
+ declare function getString(): T;
+ const a = [...getString()];
+ `,
+ errors: [
+ {
+ messageId: 'noStringSpreadInArray',
+ line: 3,
+ column: 20,
+ endColumn: 34,
+ },
+ ],
+ },
+
+ {
+ code: `
+ declare function getString(): string & { __brand: 'test' };
+ const a = [...getString()];
+ `,
+ errors: [
+ {
+ messageId: 'noStringSpreadInArray',
+ line: 3,
+ column: 20,
+ endColumn: 34,
+ },
+ ],
+ },
+
+ {
+ code: 'const o = { ...[1, 2, 3] };',
+ errors: [
+ {
+ messageId: 'noArraySpreadInObject',
+ line: 1,
+ column: 13,
+ endColumn: 25,
+ },
+ ],
+ },
+
+ {
+ code: `
+ const arr = [1, 2, 3];
+ const o = { ...arr };
+ `,
+ errors: [
+ {
+ messageId: 'noArraySpreadInObject',
+ line: 3,
+ column: 21,
+ endColumn: 27,
+ },
+ ],
+ },
+
+ {
+ code: `
+ const arr = [1, 2, 3] as const;
+ const o = { ...arr };
+ `,
+ errors: [
+ {
+ messageId: 'noArraySpreadInObject',
+ line: 3,
+ column: 21,
+ endColumn: 27,
+ },
+ ],
+ },
+
+ {
+ code: `
+ declare const arr: number[];
+ const o = { ...arr };
+ `,
+ errors: [
+ {
+ messageId: 'noArraySpreadInObject',
+ line: 3,
+ column: 21,
+ endColumn: 27,
+ },
+ ],
+ },
+
+ {
+ code: `
+ declare const arr: readonly number[];
+ const o = { ...arr };
+ `,
+ errors: [
+ {
+ messageId: 'noArraySpreadInObject',
+ line: 3,
+ column: 21,
+ endColumn: 27,
+ },
+ ],
+ },
+
+ {
+ code: `
+ declare const arr: number[] | string[];
+ const o = { ...arr };
+ `,
+ errors: [
+ {
+ messageId: 'noArraySpreadInObject',
+ line: 3,
+ column: 21,
+ endColumn: 27,
+ },
+ ],
+ },
+
+ {
+ code: `
+ declare const arr: number[] & string[];
+ const o = { ...arr };
+ `,
+ errors: [
+ {
+ messageId: 'noArraySpreadInObject',
+ line: 3,
+ column: 21,
+ endColumn: 27,
+ },
+ ],
+ },
+
+ {
+ code: `
+ declare function getArray(): number[];
+ const o = { ...getArray() };
+ `,
+ errors: [
+ {
+ messageId: 'noArraySpreadInObject',
+ line: 3,
+ column: 21,
+ endColumn: 34,
+ },
+ ],
+ },
+
+ {
+ code: `
+ declare function getArray(): readonly number[];
+ const o = { ...getArray() };
+ `,
+ errors: [
+ {
+ messageId: 'noArraySpreadInObject',
+ line: 3,
+ column: 21,
+ endColumn: 34,
+ },
+ ],
+ },
+
+ {
+ code: 'const o = { ...new Set([1, 2, 3]) };',
+ errors: [
+ {
+ messageId: 'noIterableSpreadInObject',
+ line: 1,
+ column: 13,
+ endColumn: 34,
+ },
+ ],
+ },
+
+ {
+ code: `
+ const set = new Set([1, 2, 3]);
+ const o = { ...set };
+ `,
+ errors: [
+ {
+ messageId: 'noIterableSpreadInObject',
+ line: 3,
+ column: 21,
+ endColumn: 27,
+ },
+ ],
+ },
+
+ {
+ code: `
+ declare const set: Set;
+ const o = { ...set };
+ `,
+ errors: [
+ {
+ messageId: 'noIterableSpreadInObject',
+ line: 3,
+ column: 21,
+ endColumn: 27,
+ },
+ ],
+ },
+
+ {
+ code: `
+ declare const set: ReadonlySet;
+ const o = { ...set };
+ `,
+ errors: [
+ {
+ messageId: 'noIterableSpreadInObject',
+ line: 3,
+ column: 21,
+ endColumn: 27,
+ },
+ ],
+ },
+
+ {
+ code: `
+ declare const set: Set | { a: number };
+ const o = { ...set };
+ `,
+ errors: [
+ {
+ messageId: 'noIterableSpreadInObject',
+ line: 3,
+ column: 21,
+ endColumn: 27,
+ },
+ ],
+ },
+
+ {
+ code: `
+ declare function getSet(): Set;
+ const o = { ...getSet() };
+ `,
+ errors: [
+ {
+ messageId: 'noIterableSpreadInObject',
+ line: 3,
+ column: 21,
+ endColumn: 32,
+ },
+ ],
+ },
+
+ {
+ code: `
+ const o = {
+ ...new Map([
+ ['test-1', 1],
+ ['test-2', 2],
+ ]),
+ };
+ `,
+ errors: [
+ {
+ messageId: 'noMapSpreadInObject',
+ line: 3,
+ column: 11,
+ endLine: 6,
+ endColumn: 13,
+ },
+ ],
+ },
+
+ {
+ code: `
+ const map = new Map([
+ ['test-1', 1],
+ ['test-2', 2],
+ ]);
+
+ const o = { ...map };
+ `,
+ errors: [
+ {
+ messageId: 'noMapSpreadInObject',
+ line: 7,
+ column: 21,
+ endColumn: 27,
+ },
+ ],
+ },
+
+ {
+ code: `
+ declare const map: Map;
+ const o = { ...map };
+ `,
+ errors: [
+ {
+ messageId: 'noMapSpreadInObject',
+ line: 3,
+ column: 21,
+ endColumn: 27,
+ },
+ ],
+ },
+
+ {
+ code: `
+ declare const map: ReadonlyMap;
+ const o = { ...map };
+ `,
+ errors: [
+ {
+ messageId: 'noMapSpreadInObject',
+ line: 3,
+ column: 21,
+ endColumn: 27,
+ },
+ ],
+ },
+
+ {
+ code: `
+ declare const map: WeakMap<{ a: number }, string>;
+ const o = { ...map };
+ `,
+ errors: [
+ {
+ messageId: 'noMapSpreadInObject',
+ line: 3,
+ column: 21,
+ endColumn: 27,
+ },
+ ],
+ },
+
+ {
+ code: `
+ declare const map: Map | { a: number };
+ const o = { ...map };
+ `,
+ errors: [
+ {
+ messageId: 'noMapSpreadInObject',
+ line: 3,
+ column: 21,
+ endColumn: 27,
+ },
+ ],
+ },
+
+ {
+ code: `
+ declare function getMap(): Map;
+ const o = { ...getMap() };
+ `,
+ errors: [
+ {
+ messageId: 'noMapSpreadInObject',
+ line: 3,
+ column: 21,
+ endColumn: 32,
+ },
+ ],
+ },
+
+ {
+ code: `
+ declare const a: Map & Set;
+ const o = { ...a };
+ `,
+ errors: [
+ {
+ messageId: 'noMapSpreadInObject',
+ line: 3,
+ column: 21,
+ endColumn: 25,
+ },
+ ],
+ },
+
+ {
+ code: `
+ const ref = new WeakRef({ a: 1 });
+ const o = { ...ref };
+ `,
+ errors: [
+ {
+ messageId: 'noClassInstanceSpreadInObject',
+ line: 3,
+ column: 21,
+ endColumn: 27,
+ },
+ ],
+ },
+
+ {
+ code: `
+ const promise = new Promise(() => {});
+ const o = { ...promise };
+ `,
+ errors: [
+ {
+ messageId: 'noPromiseSpreadInObject',
+ line: 3,
+ column: 21,
+ endColumn: 31,
+ },
+ ],
+ },
+
+ {
+ code: `
+ declare const maybePromise: Promise | { a: number };
+ const o = { ...maybePromise };
+ `,
+ errors: [
+ {
+ messageId: 'noPromiseSpreadInObject',
+ line: 3,
+ column: 21,
+ endColumn: 36,
+ },
+ ],
+ },
+
+ {
+ code: `
+ declare const promise: Promise & { a: number };
+ const o = { ...promise };
+ `,
+ errors: [
+ {
+ messageId: 'noPromiseSpreadInObject',
+ line: 3,
+ column: 21,
+ endColumn: 31,
+ },
+ ],
+ },
+
+ {
+ code: `
+ declare function getPromise(): Promise;
+ const o = { ...getPromise() };
+ `,
+ errors: [
+ {
+ messageId: 'noPromiseSpreadInObject',
+ line: 3,
+ column: 21,
+ endColumn: 36,
+ },
+ ],
+ },
+
+ {
+ code: `
+ declare function getPromise>(arg: T): T;
+ const o = { ...getPromise() };
+ `,
+ errors: [
+ {
+ messageId: 'noPromiseSpreadInObject',
+ line: 3,
+ column: 21,
+ endColumn: 36,
+ },
+ ],
+ },
+
+ {
+ code: `
+ function f() {}
+
+ const o = { ...f };
+ `,
+ errors: [
+ {
+ messageId: 'noFunctionSpreadInObject',
+ line: 4,
+ column: 21,
+ endColumn: 25,
+ },
+ ],
+ },
+
+ {
+ code: `
+ const f = () => {};
+
+ const o = { ...f };
+ `,
+ errors: [
+ {
+ messageId: 'noFunctionSpreadInObject',
+ line: 4,
+ column: 21,
+ endColumn: 25,
+ },
+ ],
+ },
+
+ {
+ code: `
+ declare function f(): void;
+
+ const o = { ...f };
+ `,
+ errors: [
+ {
+ messageId: 'noFunctionSpreadInObject',
+ line: 4,
+ column: 21,
+ endColumn: 25,
+ },
+ ],
+ },
+
+ {
+ code: `
+ declare function getFunction(): () => void;
+
+ const o = { ...getFunction() };
+ `,
+ errors: [
+ {
+ messageId: 'noFunctionSpreadInObject',
+ line: 4,
+ column: 21,
+ endColumn: 37,
+ },
+ ],
+ },
+
+ {
+ code: `
+ declare const f: () => void;
+
+ const o = { ...f };
+ `,
+ errors: [
+ {
+ messageId: 'noFunctionSpreadInObject',
+ line: 4,
+ column: 21,
+ endColumn: 25,
+ },
+ ],
+ },
+
+ {
+ code: `
+ declare const f: () => void | { a: number };
+
+ const o = { ...f };
+ `,
+ errors: [
+ {
+ messageId: 'noFunctionSpreadInObject',
+ line: 4,
+ column: 21,
+ endColumn: 25,
+ },
+ ],
+ },
+
+ {
+ code: `
+ function* generator() {}
+
+ const o = { ...generator };
+ `,
+ errors: [
+ {
+ messageId: 'noFunctionSpreadInObject',
+ line: 4,
+ column: 21,
+ endColumn: 33,
+ },
+ ],
+ },
+
+ {
+ code: `
+ const iterator = {
+ *[Symbol.iterator]() {
+ yield 'test';
+ },
+ };
+
+ const o = { ...iterator };
+ `,
+ errors: [
+ {
+ messageId: 'noIterableSpreadInObject',
+ line: 8,
+ column: 21,
+ endColumn: 32,
+ },
+ ],
+ },
+
+ {
+ code: `
+ type CustomIterable = {
+ [Symbol.iterator]: () => Generator;
+ };
+
+ const iterator: CustomIterable = {
+ *[Symbol.iterator]() {
+ yield 'test';
+ },
+ };
+
+ const a = { ...iterator };
+ `,
+ options: [{ allow: ['AnotherIterable'] }],
+ errors: [
+ {
+ messageId: 'noIterableSpreadInObject',
+ line: 12,
+ column: 21,
+ endColumn: 32,
+ },
+ ],
+ },
+ {
+ code: `
+ declare module 'module' {
+ export type CustomIterable = {
+ [Symbol.iterator]: () => string;
+ };
+ }
+
+ import { CustomIterable } from 'module';
+
+ declare const iterator: CustomIterable;
+
+ const a = { ...iterator };
+ `,
+ options: [
+ {
+ allow: [{ from: 'package', package: 'module', name: 'Nothing' }],
+ },
+ ],
+ errors: [
+ {
+ messageId: 'noIterableSpreadInObject',
+ line: 12,
+ column: 21,
+ endColumn: 32,
+ },
+ ],
+ },
+
+ {
+ code: `
+ declare const iterator: Iterable;
+
+ const o = { ...iterator };
+ `,
+ errors: [
+ {
+ messageId: 'noIterableSpreadInObject',
+ line: 4,
+ column: 21,
+ endColumn: 32,
+ },
+ ],
+ },
+
+ {
+ code: `
+ declare const iterator: Iterable | { a: number };
+
+ const o = { ...iterator };
+ `,
+ errors: [
+ {
+ messageId: 'noIterableSpreadInObject',
+ line: 4,
+ column: 21,
+ endColumn: 32,
+ },
+ ],
+ },
+
+ {
+ code: `
+ declare function getIterable(): Iterable;
+
+ const o = { ...getIterable() };
+ `,
+ errors: [
+ {
+ messageId: 'noIterableSpreadInObject',
+ line: 4,
+ column: 21,
+ endColumn: 37,
+ },
+ ],
+ },
+
+ {
+ code: `
+ class A {
+ [Symbol.iterator]() {
+ return {
+ next() {
+ return { done: true, value: undefined };
+ },
+ };
+ }
+ }
+
+ const a = { ...new A() };
+ `,
+ errors: [
+ {
+ messageId: 'noIterableSpreadInObject',
+ line: 12,
+ column: 21,
+ endColumn: 31,
+ },
+ ],
+ },
+
+ {
+ code: `
+ const o = { ...new Date() };
+ `,
+ errors: [
+ {
+ messageId: 'noClassInstanceSpreadInObject',
+ line: 2,
+ column: 21,
+ endColumn: 34,
+ },
+ ],
+ },
+
+ {
+ code: `
+ declare const element: HTMLElement;
+ const o = { ...element };
+ `,
+ errors: [
+ {
+ messageId: 'noClassInstanceSpreadInObject',
+ line: 3,
+ column: 21,
+ endColumn: 31,
+ },
+ ],
+ },
+
+ {
+ code: `
+ declare const regex: RegExp;
+ const o = { ...regex };
+ `,
+ errors: [
+ {
+ messageId: 'noClassInstanceSpreadInObject',
+ line: 3,
+ column: 21,
+ endColumn: 29,
+ },
+ ],
+ },
+
+ {
+ code: `
+ class A {
+ a = 1;
+ public b = 2;
+ private c = 3;
+ protected d = 4;
+ static e = 5;
+ }
+
+ const o = { ...new A() };
+ `,
+ errors: [
+ {
+ messageId: 'noClassInstanceSpreadInObject',
+ line: 10,
+ column: 21,
+ endColumn: 31,
+ },
+ ],
+ },
+
+ {
+ code: `
+ class A {
+ a = 1;
+ }
+
+ const a = new A();
+
+ const o = { ...a };
+ `,
+ errors: [
+ {
+ messageId: 'noClassInstanceSpreadInObject',
+ line: 8,
+ column: 21,
+ endColumn: 25,
+ },
+ ],
+ },
+
+ {
+ code: `
+ class A {
+ a = 1;
+ }
+
+ declare const a: A;
+
+ const o = { ...a };
+ `,
+ errors: [
+ {
+ messageId: 'noClassInstanceSpreadInObject',
+ line: 8,
+ column: 21,
+ endColumn: 25,
+ },
+ ],
+ },
+
+ {
+ code: `
+ class A {
+ a = 1;
+ }
+
+ declare function getA(): A;
+
+ const o = { ...getA() };
+ `,
+ errors: [
+ {
+ messageId: 'noClassInstanceSpreadInObject',
+ line: 8,
+ column: 21,
+ endColumn: 30,
+ },
+ ],
+ },
+
+ {
+ code: `
+ class A {
+ a = 1;
+ }
+
+ declare function getA(arg: T): T;
+
+ const o = { ...getA() };
+ `,
+ errors: [
+ {
+ messageId: 'noClassInstanceSpreadInObject',
+ line: 8,
+ column: 21,
+ endColumn: 30,
+ },
+ ],
+ },
+
+ {
+ code: `
+ class A {
+ a = 1;
+ }
+
+ class B extends A {}
+
+ const o = { ...new B() };
+ `,
+ errors: [
+ {
+ messageId: 'noClassInstanceSpreadInObject',
+ line: 8,
+ column: 21,
+ endColumn: 31,
+ },
+ ],
+ },
+
+ {
+ code: `
+ class A {
+ a = 1;
+ }
+
+ declare const a: A | { b: string };
+
+ const o = { ...a };
+ `,
+ errors: [
+ {
+ messageId: 'noClassInstanceSpreadInObject',
+ line: 8,
+ column: 21,
+ endColumn: 25,
+ },
+ ],
+ },
+
+ {
+ code: `
+ class A {
+ a = 1;
+ }
+
+ declare const a: A & { b: string };
+
+ const o = { ...a };
+ `,
+ errors: [
+ {
+ messageId: 'noClassInstanceSpreadInObject',
+ line: 8,
+ column: 21,
+ endColumn: 25,
+ },
+ ],
+ },
+
+ {
+ code: `
+ class A {}
+
+ const o = { ...A };
+ `,
+ errors: [
+ {
+ messageId: 'noClassDeclarationSpreadInObject',
+ line: 4,
+ column: 21,
+ endColumn: 25,
+ },
+ ],
+ },
+
+ {
+ code: `
+ const A = class {};
+
+ const o = { ...A };
+ `,
+ errors: [
+ {
+ messageId: 'noClassDeclarationSpreadInObject',
+ line: 4,
+ column: 21,
+ endColumn: 25,
+ },
+ ],
+ },
+
+ {
+ code: `
+ const A = Set;
+
+ const o = { ...A };
+ `,
+ errors: [
+ {
+ messageId: 'noClassDeclarationSpreadInObject',
+ line: 4,
+ column: 21,
+ endColumn: 25,
+ },
+ ],
+ },
+
+ {
+ code: `
+ const a = {
+ ...class A {
+ static value = 1;
+ nonStatic = 2;
+ },
+ };
+ `,
+ errors: [
+ {
+ messageId: 'noClassDeclarationSpreadInObject',
+ line: 3,
+ column: 11,
+ endLine: 6,
+ endColumn: 12,
+ },
+ ],
+ },
+
+ {
+ code: noFormat`
+ const a = { ...(class A { static value = 1 }) }
+ `,
+ errors: [
+ {
+ messageId: 'noClassDeclarationSpreadInObject',
+ line: 2,
+ column: 21,
+ endColumn: 54,
+ },
+ ],
+ },
+
+ {
+ code: noFormat`
+ const a = { ...new (class A { static value = 1; })() };
+ `,
+ errors: [
+ {
+ messageId: 'noClassInstanceSpreadInObject',
+ line: 2,
+ column: 21,
+ endColumn: 61,
+ },
+ ],
+ },
+ ],
+});
diff --git a/packages/eslint-plugin/tests/schema-snapshots/no-misused-spread.shot b/packages/eslint-plugin/tests/schema-snapshots/no-misused-spread.shot
new file mode 100644
index 000000000000..09a7e195ef81
--- /dev/null
+++ b/packages/eslint-plugin/tests/schema-snapshots/no-misused-spread.shot
@@ -0,0 +1,137 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Rule schemas should be convertible to TS types for documentation purposes no-misused-spread 1`] = `
+"
+# SCHEMA:
+
+[
+ {
+ "additionalProperties": false,
+ "properties": {
+ "allow": {
+ "description": "An array of type specifiers that are known to be safe to spread.",
+ "items": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "enum": ["file"],
+ "type": "string"
+ },
+ "name": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "items": {
+ "type": "string"
+ },
+ "minItems": 1,
+ "type": "array",
+ "uniqueItems": true
+ }
+ ]
+ },
+ "path": {
+ "type": "string"
+ }
+ },
+ "required": ["from", "name"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "enum": ["lib"],
+ "type": "string"
+ },
+ "name": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "items": {
+ "type": "string"
+ },
+ "minItems": 1,
+ "type": "array",
+ "uniqueItems": true
+ }
+ ]
+ }
+ },
+ "required": ["from", "name"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "enum": ["package"],
+ "type": "string"
+ },
+ "name": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "items": {
+ "type": "string"
+ },
+ "minItems": 1,
+ "type": "array",
+ "uniqueItems": true
+ }
+ ]
+ },
+ "package": {
+ "type": "string"
+ }
+ },
+ "required": ["from", "name", "package"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "type": "object"
+ }
+]
+
+
+# TYPES:
+
+type Options = [
+ {
+ /** An array of type specifiers that are known to be safe to spread. */
+ allow?: (
+ | {
+ from: 'file';
+ name: [string, ...string[]] | string;
+ path?: string;
+ }
+ | {
+ from: 'lib';
+ name: [string, ...string[]] | string;
+ }
+ | {
+ from: 'package';
+ name: [string, ...string[]] | string;
+ package: string;
+ }
+ | string
+ )[];
+ },
+];
+"
+`;
diff --git a/packages/typescript-eslint/src/configs/all.ts b/packages/typescript-eslint/src/configs/all.ts
index 1c177f1943bf..42d333f05d1e 100644
--- a/packages/typescript-eslint/src/configs/all.ts
+++ b/packages/typescript-eslint/src/configs/all.ts
@@ -87,6 +87,7 @@ export default (
'@typescript-eslint/no-meaningless-void-operator': 'error',
'@typescript-eslint/no-misused-new': 'error',
'@typescript-eslint/no-misused-promises': 'error',
+ '@typescript-eslint/no-misused-spread': 'error',
'@typescript-eslint/no-mixed-enums': 'error',
'@typescript-eslint/no-namespace': 'error',
'@typescript-eslint/no-non-null-asserted-nullish-coalescing': 'error',
diff --git a/packages/typescript-eslint/src/configs/disable-type-checked.ts b/packages/typescript-eslint/src/configs/disable-type-checked.ts
index b4c2afd20ec8..f77c90140408 100644
--- a/packages/typescript-eslint/src/configs/disable-type-checked.ts
+++ b/packages/typescript-eslint/src/configs/disable-type-checked.ts
@@ -32,6 +32,7 @@ export default (
'@typescript-eslint/no-implied-eval': 'off',
'@typescript-eslint/no-meaningless-void-operator': 'off',
'@typescript-eslint/no-misused-promises': 'off',
+ '@typescript-eslint/no-misused-spread': 'off',
'@typescript-eslint/no-mixed-enums': 'off',
'@typescript-eslint/no-redundant-type-constituents': 'off',
'@typescript-eslint/no-unnecessary-boolean-literal-compare': 'off',
diff --git a/packages/typescript-eslint/src/configs/strict-type-checked-only.ts b/packages/typescript-eslint/src/configs/strict-type-checked-only.ts
index 15a4d035ae3c..5b2ffe8fc94d 100644
--- a/packages/typescript-eslint/src/configs/strict-type-checked-only.ts
+++ b/packages/typescript-eslint/src/configs/strict-type-checked-only.ts
@@ -35,6 +35,7 @@ export default (
'@typescript-eslint/no-implied-eval': 'error',
'@typescript-eslint/no-meaningless-void-operator': 'error',
'@typescript-eslint/no-misused-promises': 'error',
+ '@typescript-eslint/no-misused-spread': 'error',
'@typescript-eslint/no-mixed-enums': 'error',
'@typescript-eslint/no-redundant-type-constituents': 'error',
'@typescript-eslint/no-unnecessary-boolean-literal-compare': 'error',
diff --git a/packages/typescript-eslint/src/configs/strict-type-checked.ts b/packages/typescript-eslint/src/configs/strict-type-checked.ts
index 99e4b05d303b..f593b1225f13 100644
--- a/packages/typescript-eslint/src/configs/strict-type-checked.ts
+++ b/packages/typescript-eslint/src/configs/strict-type-checked.ts
@@ -49,6 +49,7 @@ export default (
'@typescript-eslint/no-meaningless-void-operator': 'error',
'@typescript-eslint/no-misused-new': 'error',
'@typescript-eslint/no-misused-promises': 'error',
+ '@typescript-eslint/no-misused-spread': 'error',
'@typescript-eslint/no-mixed-enums': 'error',
'@typescript-eslint/no-namespace': 'error',
'@typescript-eslint/no-non-null-asserted-nullish-coalescing': 'error',