diff --git a/packages/eslint-plugin/docs/rules/no-misused-object-likes.mdx b/packages/eslint-plugin/docs/rules/no-misused-object-likes.mdx
new file mode 100644
index 000000000000..f81191eb523a
--- /dev/null
+++ b/packages/eslint-plugin/docs/rules/no-misused-object-likes.mdx
@@ -0,0 +1,43 @@
+---
+description: 'Disallow using `Object` methods on `Map` or `Set` 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-object-likes** for documentation.
+
+Object methods like `Object.keys` and `Object.values` are intended for plain objects. Using them on `Map` or `Set` is a common mistake that may lead to unexpected behavior.
+
+This rule disallows such usage, encouraging correct alternatives.
+
+
+
+ ```ts
+ const mySet = new Set(['foo', 'bar']);
+ Object.values(mySet);
+
+ const myMap = new Map([['foo', 'bar'], ['hello', 'world']]);
+ Object.entries(myMap);
+ ```
+
+
+
+ ```ts
+ const mySet = new Set(['foo', 'bar']);
+ mySet.keys();
+
+ const myMap = new Map([['foo', 'bar'], ['hello', 'world']]);
+ myMap.entries();
+ ```
+
+
+
+
+## When Not To Use It
+
+If your application extends the `Map` or `Set` types in unusual ways or manipulating their class prototype chains, you might not want this rule.
+
+You might consider using [ESLint disable comments](https://eslint.org/docs/latest/use/configure/rules#using-configuration-comments-1) for those specific situations 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 cd77c7dc2835..93455358e4ec 100644
--- a/packages/eslint-plugin/src/configs/all.ts
+++ b/packages/eslint-plugin/src/configs/all.ts
@@ -73,6 +73,7 @@ export = {
'@typescript-eslint/no-magic-numbers': 'error',
'@typescript-eslint/no-meaningless-void-operator': 'error',
'@typescript-eslint/no-misused-new': 'error',
+ '@typescript-eslint/no-misused-object-likes': 'error',
'@typescript-eslint/no-misused-promises': 'error',
'@typescript-eslint/no-mixed-enums': 'error',
'@typescript-eslint/no-namespace': 'error',
diff --git a/packages/eslint-plugin/src/configs/disable-type-checked.ts b/packages/eslint-plugin/src/configs/disable-type-checked.ts
index 061df20cdd65..8c8210bbee18 100644
--- a/packages/eslint-plugin/src/configs/disable-type-checked.ts
+++ b/packages/eslint-plugin/src/configs/disable-type-checked.ts
@@ -24,6 +24,7 @@ export = {
'@typescript-eslint/no-for-in-array': 'off',
'@typescript-eslint/no-implied-eval': 'off',
'@typescript-eslint/no-meaningless-void-operator': 'off',
+ '@typescript-eslint/no-misused-object-likes': 'off',
'@typescript-eslint/no-misused-promises': 'off',
'@typescript-eslint/no-mixed-enums': 'off',
'@typescript-eslint/no-redundant-type-constituents': 'off',
diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts
index de51a8bea55d..a1d3ebb0a0bf 100644
--- a/packages/eslint-plugin/src/rules/index.ts
+++ b/packages/eslint-plugin/src/rules/index.ts
@@ -52,6 +52,7 @@ import noLossOfPrecision from './no-loss-of-precision';
import noMagicNumbers from './no-magic-numbers';
import noMeaninglessVoidOperator from './no-meaningless-void-operator';
import noMisusedNew from './no-misused-new';
+import noMisusedObjectLikes from './no-misused-object-likes';
import noMisusedPromises from './no-misused-promises';
import noMixedEnums from './no-mixed-enums';
import noNamespace from './no-namespace';
@@ -183,6 +184,7 @@ const rules = {
'no-magic-numbers': noMagicNumbers,
'no-meaningless-void-operator': noMeaninglessVoidOperator,
'no-misused-new': noMisusedNew,
+ 'no-misused-object-likes': noMisusedObjectLikes,
'no-misused-promises': noMisusedPromises,
'no-mixed-enums': noMixedEnums,
'no-namespace': noNamespace,
diff --git a/packages/eslint-plugin/src/rules/no-misused-object-likes.ts b/packages/eslint-plugin/src/rules/no-misused-object-likes.ts
new file mode 100644
index 000000000000..3fdc40fb2241
--- /dev/null
+++ b/packages/eslint-plugin/src/rules/no-misused-object-likes.ts
@@ -0,0 +1,173 @@
+import type {
+ ParserServicesWithTypeInformation,
+ TSESTree,
+} from '@typescript-eslint/utils';
+import type { RuleContext } from '@typescript-eslint/utils/ts-eslint';
+import type * as ts from 'typescript';
+
+import {
+ getConstrainedTypeAtLocation,
+ isBuiltinSymbolLike,
+} from '@typescript-eslint/type-utils';
+import { AST_NODE_TYPES } from '@typescript-eslint/utils';
+
+import {
+ createRule,
+ getParserServices,
+ getStaticMemberAccessValue,
+} from '../util';
+
+type Options = [];
+
+type MessageIds =
+ | 'noMapOrSetInObjectAssign'
+ | 'noMapOrSetInObjectEntries'
+ | 'noMapOrSetInObjectHasOwn'
+ | 'noMapOrSetInObjectHasOwnProperty'
+ | 'noMapOrSetInObjectKeys'
+ | 'noMapOrSetInObjectValues';
+
+const objectConstructorMethodsMap = new Map([
+ ['assign', 'noMapOrSetInObjectAssign'],
+ ['entries', 'noMapOrSetInObjectEntries'],
+ ['hasOwn', 'noMapOrSetInObjectHasOwn'],
+ ['hasOwnProperty', 'noMapOrSetInObjectHasOwnProperty'],
+ ['keys', 'noMapOrSetInObjectKeys'],
+ ['values', 'noMapOrSetInObjectValues'],
+]);
+
+export default createRule({
+ name: 'no-misused-object-likes',
+ meta: {
+ type: 'problem',
+ docs: {
+ description:
+ 'Disallow using `Object` methods on `Map` or `Set` when it might cause unexpected behavior',
+ requiresTypeChecking: true,
+ },
+ messages: {
+ noMapOrSetInObjectAssign:
+ "Using 'Object.assign()' with a '{{type}}' may lead to unexpected results. Consider alternative approaches instead.",
+ noMapOrSetInObjectEntries:
+ "Using 'Object.entries()' on a '{{type}}' will return an empty array. Consider using the 'entries()' method instead.",
+ noMapOrSetInObjectHasOwn:
+ "Using 'Object.hasOwn()' on a '{{type}}' may lead to unexpected results. Consider using the 'has(key)' method instead.",
+ noMapOrSetInObjectHasOwnProperty:
+ "Using 'Object.hasOwnProperty()' on a '{{type}}' may lead to unexpected results. Consider using the 'has(key)' method instead.",
+ noMapOrSetInObjectKeys:
+ "Using 'Object.keys()' on a '{{type}}' will return an empty array. Consider using the 'keys()' method instead.",
+ noMapOrSetInObjectValues:
+ "Using 'Object.values()' on a '{{type}}' will return an empty array. Consider using the 'values()' method instead.",
+ },
+ schema: [],
+ },
+ defaultOptions: [],
+ create(context) {
+ const services = getParserServices(context);
+
+ return {
+ CallExpression(node): void {
+ const objectMethodName = getPotentiallyMisusedObjectConstructorMethod(
+ context,
+ services,
+ node,
+ );
+
+ if (objectMethodName == null) {
+ return;
+ }
+
+ const objectArgument = node.arguments.at(0);
+
+ if (!objectArgument) {
+ return;
+ }
+
+ const typeName = getViolatingTypeName(
+ services,
+ getConstrainedTypeAtLocation(services, objectArgument),
+ );
+
+ if (typeName) {
+ const messageId = objectConstructorMethodsMap.get(objectMethodName);
+
+ if (messageId) {
+ context.report({
+ node,
+ messageId,
+ data: {
+ type: typeName,
+ },
+ });
+ }
+ }
+ },
+ };
+ },
+});
+
+function getViolatingTypeName(
+ services: ParserServicesWithTypeInformation,
+ type: ts.Type,
+): string | null {
+ if (isSet(services.program, type)) {
+ return 'Set';
+ }
+
+ if (isMap(services.program, type)) {
+ return 'Map';
+ }
+
+ return null;
+}
+
+function isMap(program: ts.Program, type: ts.Type): boolean {
+ return isTypeRecurser(type, t =>
+ isBuiltinSymbolLike(program, t, ['Map', 'ReadonlyMap', 'WeakMap']),
+ );
+}
+
+function isSet(program: ts.Program, type: ts.Type): boolean {
+ return isTypeRecurser(type, t =>
+ isBuiltinSymbolLike(program, t, ['Set', 'ReadonlySet', 'WeakSet']),
+ );
+}
+
+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);
+}
+
+function getPotentiallyMisusedObjectConstructorMethod(
+ context: RuleContext,
+ services: ParserServicesWithTypeInformation,
+ node: TSESTree.CallExpression,
+): string | null {
+ if (node.callee.type !== AST_NODE_TYPES.MemberExpression) {
+ return null;
+ }
+
+ const objectType = getConstrainedTypeAtLocation(services, node.callee.object);
+
+ if (!isBuiltinSymbolLike(services.program, objectType, 'ObjectConstructor')) {
+ return null;
+ }
+
+ const staticAccessValue = getStaticMemberAccessValue(node.callee, context);
+
+ if (typeof staticAccessValue !== 'string') {
+ return null;
+ }
+
+ if (!objectConstructorMethodsMap.has(staticAccessValue)) {
+ return null;
+ }
+
+ return staticAccessValue;
+}
diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-misused-object-likes.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-misused-object-likes.shot
new file mode 100644
index 000000000000..07b8f3d2c420
--- /dev/null
+++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-misused-object-likes.shot
@@ -0,0 +1,25 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Validating rule docs no-misused-object-likes.mdx code examples ESLint output 1`] = `
+"Incorrect
+
+const mySet = new Set(['foo', 'bar']);
+ Object.values(mySet);
+ ~~~~~~~~~~~~~~~~~~~~ Using 'Object.values()' on a 'Set' will return an empty array. Consider using the 'values()' method instead.
+
+const myMap = new Map([['foo', 'bar'], ['hello', 'world']]);
+ Object.entries(myMap);
+ ~~~~~~~~~~~~~~~~~~~~~ Using 'Object.entries()' on a 'Map' will return an empty array. Consider using the 'entries()' method instead.
+"
+`;
+
+exports[`Validating rule docs no-misused-object-likes.mdx code examples ESLint output 2`] = `
+"Correct
+
+const mySet = new Set(['foo', 'bar']);
+ mySet.keys();
+
+const myMap = new Map([['foo', 'bar'], ['hello', 'world']]);
+ myMap.entries();
+"
+`;
diff --git a/packages/eslint-plugin/tests/rules/no-misused-object-likes.test.ts b/packages/eslint-plugin/tests/rules/no-misused-object-likes.test.ts
new file mode 100644
index 000000000000..3cc37818ead9
--- /dev/null
+++ b/packages/eslint-plugin/tests/rules/no-misused-object-likes.test.ts
@@ -0,0 +1,329 @@
+import { RuleTester } from '@typescript-eslint/rule-tester';
+
+import rule from '../../src/rules/no-misused-object-likes';
+import { getFixturesRootDir } from '../RuleTester';
+
+const rootPath = getFixturesRootDir();
+
+const ruleTester = new RuleTester({
+ languageOptions: {
+ parserOptions: {
+ project: './tsconfig.json',
+ tsconfigRootDir: rootPath,
+ },
+ },
+});
+
+ruleTester.run('no-misused-object-likes', rule, {
+ valid: [
+ "Object.keys({ a: 'a' });",
+ 'Object.keys([1, 2, 3]);',
+ 'Object.keys([1, 2, 3] as const);',
+ `
+declare const data: unknown;
+Object.keys(data);
+ `,
+ `
+declare const data: any;
+Object.keys(data);
+ `,
+ `
+declare const data: [1, 2, 3];
+Object.keys(data);
+ `,
+ `
+declare const data: Set;
+data.keys();
+ `,
+ `
+declare const data: Map;
+data.keys();
+ `,
+ `
+declare const data: Set;
+Object.keys(data);
+ `,
+ `
+declare const data: Map;
+Object.keys(data);
+ `,
+ `
+declare const data: Map;
+Object.keys(data);
+ `,
+ `
+function test(data: T) {
+ Object.keys(data);
+}
+ `,
+ `
+function test(data: string[]) {
+ Object.keys(data);
+}
+ `,
+ `
+function test(data: Record) {
+ Object.keys(data);
+}
+ `,
+ `
+declare const data: Iterable;
+Object.keys(data);
+ `,
+ // error type
+ `
+Object.keys(data);
+ `,
+ `
+declare const data: Set;
+keys(data);
+ `,
+ `
+declare const data: Set;
+Object.create(data);
+ `,
+ `
+declare const data: Set;
+Object[Symbol.iterator](data);
+ `,
+ `
+Object.keys();
+ `,
+ ],
+ invalid: [
+ {
+ code: `
+declare const data: Set;
+Object.keys(data);
+ `,
+ errors: [
+ {
+ column: 1,
+ data: {
+ type: 'Set',
+ },
+ line: 3,
+ messageId: 'noMapOrSetInObjectKeys',
+ },
+ ],
+ },
+ {
+ code: `
+declare const data: Set;
+Object.values(data, 'extra-arg');
+ `,
+ errors: [
+ {
+ column: 1,
+ data: {
+ type: 'Set',
+ },
+ line: 3,
+ messageId: 'noMapOrSetInObjectValues',
+ },
+ ],
+ },
+ {
+ code: `
+declare const data: Set | { a: number };
+Object.assign(data);
+ `,
+ errors: [
+ {
+ column: 1,
+ data: {
+ type: 'Set',
+ },
+ line: 3,
+ messageId: 'noMapOrSetInObjectAssign',
+ },
+ ],
+ },
+ {
+ code: `
+declare const data:
+ | { a: number }
+ | ({ b: boolean } | ({ c: string } & Set));
+Object.entries(data);
+ `,
+ errors: [
+ {
+ column: 1,
+ data: {
+ type: 'Set',
+ },
+ line: 5,
+ messageId: 'noMapOrSetInObjectEntries',
+ },
+ ],
+ },
+ {
+ code: `
+function test>(data: T) {
+ Object.hasOwn(data, 'key');
+}
+ `,
+ errors: [
+ {
+ column: 3,
+ data: {
+ type: 'Set',
+ },
+ line: 3,
+ messageId: 'noMapOrSetInObjectHasOwn',
+ },
+ ],
+ },
+ {
+ code: `
+class ExtendedSet extends Set {}
+
+declare const data: ExtendedSet;
+Object.hasOwnProperty(data, 'key');
+ `,
+ errors: [
+ {
+ column: 1,
+ data: {
+ type: 'Set',
+ },
+ line: 5,
+ messageId: 'noMapOrSetInObjectHasOwnProperty',
+ },
+ ],
+ },
+ {
+ code: `
+declare const data: Set;
+Object['values'](data);
+ `,
+ errors: [
+ {
+ column: 1,
+ data: {
+ type: 'Set',
+ },
+ line: 3,
+ messageId: 'noMapOrSetInObjectValues',
+ },
+ ],
+ },
+ {
+ code: `
+declare const data: Map;
+Object.keys(data);
+ `,
+ errors: [
+ {
+ column: 1,
+ data: {
+ type: 'Map',
+ },
+ line: 3,
+ messageId: 'noMapOrSetInObjectKeys',
+ },
+ ],
+ },
+ {
+ code: `
+declare const data: Map;
+Object.assign(data, 'extra-arg');
+ `,
+ errors: [
+ {
+ column: 1,
+ data: {
+ type: 'Map',
+ },
+ line: 3,
+ messageId: 'noMapOrSetInObjectAssign',
+ },
+ ],
+ },
+ {
+ code: `
+declare const data: Map | { a: string };
+Object.entries(data);
+ `,
+ errors: [
+ {
+ column: 1,
+ data: {
+ type: 'Map',
+ },
+ line: 3,
+ messageId: 'noMapOrSetInObjectEntries',
+ },
+ ],
+ },
+ {
+ code: `
+declare const data:
+ | { a: number }
+ | ({ b: boolean } | ({ c: string } & Map));
+Object.keys(data);
+ `,
+ errors: [
+ {
+ column: 1,
+ data: {
+ type: 'Map',
+ },
+ line: 5,
+ messageId: 'noMapOrSetInObjectKeys',
+ },
+ ],
+ },
+ {
+ code: `
+function test>(data: T) {
+ Object.hasOwn(data, 'foo');
+}
+ `,
+ errors: [
+ {
+ column: 3,
+ data: {
+ type: 'Map',
+ },
+ line: 3,
+ messageId: 'noMapOrSetInObjectHasOwn',
+ },
+ ],
+ },
+ {
+ code: `
+class ExtendedMap extends Map {}
+
+declare const data: ExtendedMap;
+Object.values(data);
+ `,
+ errors: [
+ {
+ column: 1,
+ data: {
+ type: 'Map',
+ },
+ line: 5,
+ messageId: 'noMapOrSetInObjectValues',
+ },
+ ],
+ },
+ {
+ code: `
+declare const data: Map;
+Object['keys'](data);
+ `,
+ errors: [
+ {
+ column: 1,
+ data: {
+ type: 'Map',
+ },
+ line: 3,
+ messageId: 'noMapOrSetInObjectKeys',
+ },
+ ],
+ },
+ ],
+});
diff --git a/packages/eslint-plugin/tests/schema-snapshots/no-misused-object-likes.shot b/packages/eslint-plugin/tests/schema-snapshots/no-misused-object-likes.shot
new file mode 100644
index 000000000000..6c5b84e1e34e
--- /dev/null
+++ b/packages/eslint-plugin/tests/schema-snapshots/no-misused-object-likes.shot
@@ -0,0 +1,14 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Rule schemas should be convertible to TS types for documentation purposes no-misused-object-likes 1`] = `
+"
+# SCHEMA:
+
+[]
+
+
+# TYPES:
+
+/** No options declared */
+type Options = [];"
+`;
diff --git a/packages/typescript-eslint/src/configs/all.ts b/packages/typescript-eslint/src/configs/all.ts
index cd16389445a5..63eb9f363891 100644
--- a/packages/typescript-eslint/src/configs/all.ts
+++ b/packages/typescript-eslint/src/configs/all.ts
@@ -86,6 +86,7 @@ export default (
'@typescript-eslint/no-magic-numbers': 'error',
'@typescript-eslint/no-meaningless-void-operator': 'error',
'@typescript-eslint/no-misused-new': 'error',
+ '@typescript-eslint/no-misused-object-likes': 'error',
'@typescript-eslint/no-misused-promises': 'error',
'@typescript-eslint/no-mixed-enums': 'error',
'@typescript-eslint/no-namespace': 'error',
diff --git a/packages/typescript-eslint/src/configs/disable-type-checked.ts b/packages/typescript-eslint/src/configs/disable-type-checked.ts
index eeb80399882c..e604ea49a7b3 100644
--- a/packages/typescript-eslint/src/configs/disable-type-checked.ts
+++ b/packages/typescript-eslint/src/configs/disable-type-checked.ts
@@ -31,6 +31,7 @@ export default (
'@typescript-eslint/no-for-in-array': 'off',
'@typescript-eslint/no-implied-eval': 'off',
'@typescript-eslint/no-meaningless-void-operator': 'off',
+ '@typescript-eslint/no-misused-object-likes': 'off',
'@typescript-eslint/no-misused-promises': 'off',
'@typescript-eslint/no-mixed-enums': 'off',
'@typescript-eslint/no-redundant-type-constituents': 'off',