From bcfbb03082884c4766845c705d8ef0133a08fa57 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Tue, 23 Apr 2024 15:13:08 -0400 Subject: [PATCH 01/21] feat(eslint-plugin): split no-empty-object-type rule out from ban-types rule --- .../eslint-plugin/docs/rules/ban-types.mdx | 9 -- .../docs/rules/no-empty-interface.mdx | 4 + .../docs/rules/no-empty-object-type.mdx | 65 ++++++++++++++ packages/eslint-plugin/src/configs/all.ts | 1 + .../src/configs/recommended-type-checked.ts | 1 + .../eslint-plugin/src/configs/recommended.ts | 1 + .../src/configs/strict-type-checked.ts | 1 + packages/eslint-plugin/src/configs/strict.ts | 1 + packages/eslint-plugin/src/rules/ban-types.ts | 31 +------ packages/eslint-plugin/src/rules/index.ts | 2 + .../src/rules/no-empty-object-type.ts | 45 ++++++++++ .../ban-types.shot | 26 +----- .../no-empty-object-type.shot | 34 ++++++++ .../tests/rules/ban-types.test.ts | 38 --------- .../tests/rules/no-empty-object-type.test.ts | 85 +++++++++++++++++++ .../no-empty-object-type.shot | 14 +++ packages/typescript-eslint/src/configs/all.ts | 1 + .../src/configs/recommended-type-checked.ts | 1 + .../src/configs/recommended.ts | 1 + .../src/configs/strict-type-checked.ts | 1 + .../typescript-eslint/src/configs/strict.ts | 1 + 21 files changed, 265 insertions(+), 98 deletions(-) create mode 100644 packages/eslint-plugin/docs/rules/no-empty-object-type.mdx create mode 100644 packages/eslint-plugin/src/rules/no-empty-object-type.ts create mode 100644 packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-empty-object-type.shot create mode 100644 packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts create mode 100644 packages/eslint-plugin/tests/schema-snapshots/no-empty-object-type.shot diff --git a/packages/eslint-plugin/docs/rules/ban-types.mdx b/packages/eslint-plugin/docs/rules/ban-types.mdx index da30f4959116..98ce57edd719 100644 --- a/packages/eslint-plugin/docs/rules/ban-types.mdx +++ b/packages/eslint-plugin/docs/rules/ban-types.mdx @@ -36,9 +36,6 @@ const func: Function = () => 1; // use safer object types const lowerObj: Object = {}; const capitalObj: Object = { a: 'string' }; - -const curly1: {} = 1; -const curly2: {} = { a: 'string' }; ``` @@ -58,9 +55,6 @@ const func: () => number = () => 1; // use safer object types const lowerObj: object = {}; const capitalObj: { a: string } = { a: 'string' }; - -const curly1: number = 1; -const curly2: Record<'a', string> = { a: 'string' }; ``` @@ -74,9 +68,6 @@ The default options provide a set of "best practices", intended to provide safet - Avoid the `Function` type, as it provides little safety for the following reasons: - It provides no type safety when calling the value, which means it's easy to provide the wrong arguments. - It accepts class declarations, which will fail when called, as they are called without the `new` keyword. -- Avoid the `Object` and `{}` types, as they mean "any non-nullish value". - - This is a point of confusion for many developers, who think it means "any object type". - - See [this comment for more information](https://github.com/typescript-eslint/typescript-eslint/issues/2063#issuecomment-675156492).
Default Options diff --git a/packages/eslint-plugin/docs/rules/no-empty-interface.mdx b/packages/eslint-plugin/docs/rules/no-empty-interface.mdx index ad240237ddb8..aec497481242 100644 --- a/packages/eslint-plugin/docs/rules/no-empty-interface.mdx +++ b/packages/eslint-plugin/docs/rules/no-empty-interface.mdx @@ -61,3 +61,7 @@ interface Baz extends Foo, Bar {} ## When Not To Use It If you don't care about having empty/meaningless interfaces, then you will not need this rule. + +## Related To + +- [`no-empty-object-type`](./no-empty-object-type.mdx) diff --git a/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx b/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx new file mode 100644 index 000000000000..ffb2cb76324e --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx @@ -0,0 +1,65 @@ +--- +description: 'Disallow accidentally using the "empty object" type.' +--- + +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-empty-object-type** for documentation. + +The `{}`, or "empty object" type in TypeScript is a common source of confusion for developers unfamiliar with TypeScript's structural typing. +`{}` represents any _non-nullish value_, including literals like `0` and `""`: + +```ts +let anyNonNullishValue: {} = 'Intentionally allowed by TypeScript.'; +``` + +Often, developers writing `{}` actually mean either: + +- `object`: representing any _object_ value +- `unknown`: representing any value _other than `null` and `undefined`_ + +To avoid confusion around the `{}` type allowing non-object values, this rule bans usage of the `{}` type. + +:::tip +If you do have a use case for an API allowing any _non-nullish value_, you can always use an [ESLint disable comment](https://eslint.org/docs/latest/use/configure/rules#using-configuration-comments-1) or [disable the rule in your ESLint config](https://eslint.org/docs/latest/use/configure/rules#using-configuration-files-1). +::: + +## Examples + + + + +```ts +let anyObject: {}; +let anyValue: {}; +let emptyObject: {}; +``` + + + + +```ts +let anyObject: object; +let anyValue: unknown; +let emptyObject: Record; +``` + + + + +## When Not To Use It + +If your code commonly needs to represent the _"any non-nullish value"_ type, this rule may not be for you. +Projects that extensively use type operations such as conditional types and mapped types oftentimes benefit from disabling this rule. + +## Further Reading + +- [Enhancement: [ban-types] Split the {} ban into a separate, better-phrased rule](https://github.com/typescript-eslint/typescript-eslint/issues/8700) +- [The Empty Object Type in TypeScript](https://www.totaltypescript.com/the-empty-object-type-in-typescript) + +## Related To + +- [`no-empty-interface`](./no-empty-interface.mdx) diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index 71db2c523b2b..a9eb685a9c3a 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -55,6 +55,7 @@ export = { 'no-empty-function': 'off', '@typescript-eslint/no-empty-function': 'error', '@typescript-eslint/no-empty-interface': 'error', + '@typescript-eslint/no-empty-object-type': 'error', '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/no-extra-non-null-assertion': 'error', '@typescript-eslint/no-extraneous-class': 'error', diff --git a/packages/eslint-plugin/src/configs/recommended-type-checked.ts b/packages/eslint-plugin/src/configs/recommended-type-checked.ts index 20993a066405..858eb6555090 100644 --- a/packages/eslint-plugin/src/configs/recommended-type-checked.ts +++ b/packages/eslint-plugin/src/configs/recommended-type-checked.ts @@ -18,6 +18,7 @@ export = { '@typescript-eslint/no-base-to-string': 'error', '@typescript-eslint/no-duplicate-enum-values': 'error', '@typescript-eslint/no-duplicate-type-constituents': 'error', + '@typescript-eslint/no-empty-object-type': 'error', '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/no-extra-non-null-assertion': 'error', '@typescript-eslint/no-floating-promises': 'error', diff --git a/packages/eslint-plugin/src/configs/recommended.ts b/packages/eslint-plugin/src/configs/recommended.ts index 58f31702ada3..c93e38eabb2f 100644 --- a/packages/eslint-plugin/src/configs/recommended.ts +++ b/packages/eslint-plugin/src/configs/recommended.ts @@ -15,6 +15,7 @@ export = { 'no-array-constructor': 'off', '@typescript-eslint/no-array-constructor': 'error', '@typescript-eslint/no-duplicate-enum-values': 'error', + '@typescript-eslint/no-empty-object-type': 'error', '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/no-extra-non-null-assertion': 'error', '@typescript-eslint/no-misused-new': 'error', diff --git a/packages/eslint-plugin/src/configs/strict-type-checked.ts b/packages/eslint-plugin/src/configs/strict-type-checked.ts index 5b00d2369842..91fd8b1589df 100644 --- a/packages/eslint-plugin/src/configs/strict-type-checked.ts +++ b/packages/eslint-plugin/src/configs/strict-type-checked.ts @@ -24,6 +24,7 @@ export = { '@typescript-eslint/no-duplicate-enum-values': 'error', '@typescript-eslint/no-duplicate-type-constituents': 'error', '@typescript-eslint/no-dynamic-delete': 'error', + '@typescript-eslint/no-empty-object-type': 'error', '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/no-extra-non-null-assertion': 'error', '@typescript-eslint/no-extraneous-class': 'error', diff --git a/packages/eslint-plugin/src/configs/strict.ts b/packages/eslint-plugin/src/configs/strict.ts index 598b3246e270..ae000f72d3f5 100644 --- a/packages/eslint-plugin/src/configs/strict.ts +++ b/packages/eslint-plugin/src/configs/strict.ts @@ -19,6 +19,7 @@ export = { '@typescript-eslint/no-array-constructor': 'error', '@typescript-eslint/no-duplicate-enum-values': 'error', '@typescript-eslint/no-dynamic-delete': 'error', + '@typescript-eslint/no-empty-object-type': 'error', '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/no-extra-non-null-assertion': 'error', '@typescript-eslint/no-extraneous-class': 'error', diff --git a/packages/eslint-plugin/src/rules/ban-types.ts b/packages/eslint-plugin/src/rules/ban-types.ts index da2d79716a35..06c9daa62d4a 100644 --- a/packages/eslint-plugin/src/rules/ban-types.ts +++ b/packages/eslint-plugin/src/rules/ban-types.ts @@ -73,7 +73,10 @@ const defaultTypes: Types = { message: 'Use bigint instead', fixWith: 'bigint', }, - + Object: { + message: 'Use object instead', + fixWith: 'object', + }, Function: { message: [ 'The `Function` type accepts any function-like value.', @@ -82,32 +85,6 @@ const defaultTypes: Types = { 'If you are expecting the function to accept certain arguments, you should explicitly define the function shape.', ].join('\n'), }, - - // object typing - Object: { - message: [ - 'The `Object` type actually means "any non-nullish value", so it is marginally better than `unknown`.', - '- If you want a type meaning "any object", you probably want `object` instead.', - '- If you want a type meaning "any value", you probably want `unknown` instead.', - '- If you really want a type meaning "any non-nullish value", you probably want `NonNullable` instead.', - ].join('\n'), - suggest: ['object', 'unknown', 'NonNullable'], - }, - '{}': { - message: [ - '`{}` actually means "any non-nullish value".', - '- If you want a type meaning "any object", you probably want `object` instead.', - '- If you want a type meaning "any value", you probably want `unknown` instead.', - '- If you want a type meaning "empty object", you probably want `Record` instead.', - '- If you really want a type meaning "any non-nullish value", you probably want `NonNullable` instead.', - ].join('\n'), - suggest: [ - 'object', - 'unknown', - 'Record', - 'NonNullable', - ], - }, }; export const TYPE_KEYWORDS = { diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index ae1da2b6d438..c580027d059c 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -36,6 +36,7 @@ import noDuplicateTypeConstituents from './no-duplicate-type-constituents'; import noDynamicDelete from './no-dynamic-delete'; import noEmptyFunction from './no-empty-function'; import noEmptyInterface from './no-empty-interface'; +import noEmptyObjectType from './no-empty-object-type'; import noExplicitAny from './no-explicit-any'; import noExtraNonNullAssertion from './no-extra-non-null-assertion'; import noExtraneousClass from './no-extraneous-class'; @@ -160,6 +161,7 @@ export default { 'no-dynamic-delete': noDynamicDelete, 'no-empty-function': noEmptyFunction, 'no-empty-interface': noEmptyInterface, + 'no-empty-object-type': noEmptyObjectType, 'no-explicit-any': noExplicitAny, 'no-extra-non-null-assertion': noExtraNonNullAssertion, 'no-extraneous-class': noExtraneousClass, diff --git a/packages/eslint-plugin/src/rules/no-empty-object-type.ts b/packages/eslint-plugin/src/rules/no-empty-object-type.ts new file mode 100644 index 000000000000..d4d784477b91 --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-empty-object-type.ts @@ -0,0 +1,45 @@ +import type { TSESLint } from '@typescript-eslint/utils'; + +import { createRule } from '../util'; + +export default createRule({ + name: 'no-empty-object-type', + meta: { + type: 'suggestion', + docs: { + description: 'Disallow accidentally using the "empty object" type', + recommended: 'recommended', + }, + hasSuggestions: true, + messages: { + banEmptyObjectType: [ + 'The `{}` ("empty object") type allows any non-nullish value, including literals like `0` and `""`.', + "- If that's what you want, disable this lint rule with an inline comment or in your ESLint config.", + '- If you want a type meaning "any object", you probably want `object` instead.', + '- If you want a type meaning "any value", you probably want `unknown` instead.', + '- If you want a type meaning "empty object", you probably want `Record` instead.', + ].join('\n'), + replaceEmptyObjectType: 'Replace `{}` with `{{replacement}}`.', + }, + schema: [], + }, + defaultOptions: [], + create(context) { + return { + 'TSTypeLiteral[members.length=0]'(node): void { + context.report({ + messageId: 'banEmptyObjectType', + node, + suggest: ['object', 'unknown', 'Record'].map( + replacement => ({ + data: { replacement }, + messageId: 'replaceEmptyObjectType', + fix: (fixer): TSESLint.RuleFix => + fixer.replaceText(node, replacement), + }), + ), + }); + }, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/ban-types.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/ban-types.shot index f96ac650ffa5..6644fd985d9b 100644 --- a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/ban-types.shot +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/ban-types.shot @@ -24,28 +24,9 @@ const func: Function = () => 1; // use safer object types const lowerObj: Object = {}; - ~~~~~~ Don't use \`Object\` as a type. The \`Object\` type actually means "any non-nullish value", so it is marginally better than \`unknown\`. - - If you want a type meaning "any object", you probably want \`object\` instead. - - If you want a type meaning "any value", you probably want \`unknown\` instead. - - If you really want a type meaning "any non-nullish value", you probably want \`NonNullable\` instead. + ~~~~~~ Don't use \`Object\` as a type. Use object instead const capitalObj: Object = { a: 'string' }; - ~~~~~~ Don't use \`Object\` as a type. The \`Object\` type actually means "any non-nullish value", so it is marginally better than \`unknown\`. - - If you want a type meaning "any object", you probably want \`object\` instead. - - If you want a type meaning "any value", you probably want \`unknown\` instead. - - If you really want a type meaning "any non-nullish value", you probably want \`NonNullable\` instead. - -const curly1: {} = 1; - ~~ Don't use \`{}\` as a type. \`{}\` actually means "any non-nullish value". - - If you want a type meaning "any object", you probably want \`object\` instead. - - If you want a type meaning "any value", you probably want \`unknown\` instead. - - If you want a type meaning "empty object", you probably want \`Record\` instead. - - If you really want a type meaning "any non-nullish value", you probably want \`NonNullable\` instead. -const curly2: {} = { a: 'string' }; - ~~ Don't use \`{}\` as a type. \`{}\` actually means "any non-nullish value". - - If you want a type meaning "any object", you probably want \`object\` instead. - - If you want a type meaning "any value", you probably want \`unknown\` instead. - - If you want a type meaning "empty object", you probably want \`Record\` instead. - - If you really want a type meaning "any non-nullish value", you probably want \`NonNullable\` instead. + ~~~~~~ Don't use \`Object\` as a type. Use object instead " `; @@ -65,8 +46,5 @@ const func: () => number = () => 1; // use safer object types const lowerObj: object = {}; const capitalObj: { a: string } = { a: 'string' }; - -const curly1: number = 1; -const curly2: Record<'a', string> = { a: 'string' }; " `; diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-empty-object-type.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-empty-object-type.shot new file mode 100644 index 000000000000..1b1eadc79d09 --- /dev/null +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-empty-object-type.shot @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Validating rule docs no-empty-object-type.mdx code examples ESLint output 1`] = ` +"Incorrect + +let anyObject: {}; + ~~ The \`{}\` ("empty object") type allows any non-nullish value, including literals like \`0\` and \`""\`. + - If that's what you want, disable this lint rule with an inline comment or in your ESLint config. + - If you want a type meaning "any object", you probably want \`object\` instead. + - If you want a type meaning "any value", you probably want \`unknown\` instead. + - If you want a type meaning "empty object", you probably want \`Record\` instead. +let anyValue: {}; + ~~ The \`{}\` ("empty object") type allows any non-nullish value, including literals like \`0\` and \`""\`. + - If that's what you want, disable this lint rule with an inline comment or in your ESLint config. + - If you want a type meaning "any object", you probably want \`object\` instead. + - If you want a type meaning "any value", you probably want \`unknown\` instead. + - If you want a type meaning "empty object", you probably want \`Record\` instead. +let emptyObject: {}; + ~~ The \`{}\` ("empty object") type allows any non-nullish value, including literals like \`0\` and \`""\`. + - If that's what you want, disable this lint rule with an inline comment or in your ESLint config. + - If you want a type meaning "any object", you probably want \`object\` instead. + - If you want a type meaning "any value", you probably want \`unknown\` instead. + - If you want a type meaning "empty object", you probably want \`Record\` instead. +" +`; + +exports[`Validating rule docs no-empty-object-type.mdx code examples ESLint output 2`] = ` +"Correct + +let anyObject: object; +let anyValue: unknown; +let emptyObject: Record; +" +`; diff --git a/packages/eslint-plugin/tests/rules/ban-types.test.ts b/packages/eslint-plugin/tests/rules/ban-types.test.ts index 5f8d72ab0f76..046ac290a635 100644 --- a/packages/eslint-plugin/tests/rules/ban-types.test.ts +++ b/packages/eslint-plugin/tests/rules/ban-types.test.ts @@ -137,44 +137,6 @@ ruleTester.run('ban-types', rule, { ], options, }, - { - code: 'let a: Object;', - output: null, - errors: [ - { - messageId: 'bannedTypeMessage', - data: { - name: 'Object', - customMessage: [ - ' The `Object` type actually means "any non-nullish value", so it is marginally better than `unknown`.', - '- If you want a type meaning "any object", you probably want `object` instead.', - '- If you want a type meaning "any value", you probably want `unknown` instead.', - '- If you really want a type meaning "any non-nullish value", you probably want `NonNullable` instead.', - ].join('\n'), - }, - line: 1, - column: 8, - suggestions: [ - { - messageId: 'bannedTypeReplacement', - data: { name: 'Object', replacement: 'object' }, - output: 'let a: object;', - }, - { - messageId: 'bannedTypeReplacement', - data: { name: 'Object', replacement: 'unknown' }, - output: 'let a: unknown;', - }, - { - messageId: 'bannedTypeReplacement', - data: { name: 'Object', replacement: 'NonNullable' }, - output: 'let a: NonNullable;', - }, - ], - }, - ], - options: [{}], - }, { code: 'let aa: Foo;', output: null, diff --git a/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts b/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts new file mode 100644 index 000000000000..fef527edaba3 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts @@ -0,0 +1,85 @@ +import { RuleTester } from '@typescript-eslint/rule-tester'; + +import rule from '../../src/rules/no-empty-object-type'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('no-empty-object-type', rule, { + valid: [ + 'let value: object;', + 'let value: Object;', + 'let value: { inner: true };', + ], + invalid: [ + { + code: 'let value: {};', + errors: [ + { + column: 12, + line: 1, + endColumn: 14, + endLine: 1, + messageId: 'banEmptyObjectType', + suggestions: [ + { + data: { replacement: 'object' }, + messageId: 'replaceEmptyObjectType', + output: 'let value: object;', + }, + { + data: { replacement: 'unknown' }, + messageId: 'replaceEmptyObjectType', + output: 'let value: unknown;', + }, + { + data: { replacement: 'Record' }, + messageId: 'replaceEmptyObjectType', + output: 'let value: Record;', + }, + ], + }, + ], + }, + { + code: ` +let value: { + /* ... */ +}; + `, + errors: [ + { + line: 2, + endLine: 4, + column: 12, + endColumn: 2, + messageId: 'banEmptyObjectType', + suggestions: [ + { + data: { replacement: 'object' }, + messageId: 'replaceEmptyObjectType', + output: ` +let value: object; + `, + }, + { + data: { replacement: 'unknown' }, + messageId: 'replaceEmptyObjectType', + output: ` +let value: unknown; + `, + }, + { + data: { replacement: 'Record' }, + messageId: 'replaceEmptyObjectType', + output: ` +let value: Record; + `, + }, + ], + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin/tests/schema-snapshots/no-empty-object-type.shot b/packages/eslint-plugin/tests/schema-snapshots/no-empty-object-type.shot new file mode 100644 index 000000000000..8f658c6d922b --- /dev/null +++ b/packages/eslint-plugin/tests/schema-snapshots/no-empty-object-type.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-empty-object-type 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 e555dd197e5e..5341ff26fd21 100644 --- a/packages/typescript-eslint/src/configs/all.ts +++ b/packages/typescript-eslint/src/configs/all.ts @@ -64,6 +64,7 @@ export default ( 'no-empty-function': 'off', '@typescript-eslint/no-empty-function': 'error', '@typescript-eslint/no-empty-interface': 'error', + '@typescript-eslint/no-empty-object-type': 'error', '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/no-extra-non-null-assertion': 'error', '@typescript-eslint/no-extraneous-class': 'error', diff --git a/packages/typescript-eslint/src/configs/recommended-type-checked.ts b/packages/typescript-eslint/src/configs/recommended-type-checked.ts index 5dc2fe5a1245..2d954c705819 100644 --- a/packages/typescript-eslint/src/configs/recommended-type-checked.ts +++ b/packages/typescript-eslint/src/configs/recommended-type-checked.ts @@ -27,6 +27,7 @@ export default ( '@typescript-eslint/no-base-to-string': 'error', '@typescript-eslint/no-duplicate-enum-values': 'error', '@typescript-eslint/no-duplicate-type-constituents': 'error', + '@typescript-eslint/no-empty-object-type': 'error', '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/no-extra-non-null-assertion': 'error', '@typescript-eslint/no-floating-promises': 'error', diff --git a/packages/typescript-eslint/src/configs/recommended.ts b/packages/typescript-eslint/src/configs/recommended.ts index d3aefe296189..7df78599ea91 100644 --- a/packages/typescript-eslint/src/configs/recommended.ts +++ b/packages/typescript-eslint/src/configs/recommended.ts @@ -24,6 +24,7 @@ export default ( 'no-array-constructor': 'off', '@typescript-eslint/no-array-constructor': 'error', '@typescript-eslint/no-duplicate-enum-values': 'error', + '@typescript-eslint/no-empty-object-type': 'error', '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/no-extra-non-null-assertion': 'error', '@typescript-eslint/no-misused-new': 'error', diff --git a/packages/typescript-eslint/src/configs/strict-type-checked.ts b/packages/typescript-eslint/src/configs/strict-type-checked.ts index 0f786b3f401f..1542bf528504 100644 --- a/packages/typescript-eslint/src/configs/strict-type-checked.ts +++ b/packages/typescript-eslint/src/configs/strict-type-checked.ts @@ -33,6 +33,7 @@ export default ( '@typescript-eslint/no-duplicate-enum-values': 'error', '@typescript-eslint/no-duplicate-type-constituents': 'error', '@typescript-eslint/no-dynamic-delete': 'error', + '@typescript-eslint/no-empty-object-type': 'error', '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/no-extra-non-null-assertion': 'error', '@typescript-eslint/no-extraneous-class': 'error', diff --git a/packages/typescript-eslint/src/configs/strict.ts b/packages/typescript-eslint/src/configs/strict.ts index 0406dc76e0f8..680813d7d64f 100644 --- a/packages/typescript-eslint/src/configs/strict.ts +++ b/packages/typescript-eslint/src/configs/strict.ts @@ -28,6 +28,7 @@ export default ( '@typescript-eslint/no-array-constructor': 'error', '@typescript-eslint/no-duplicate-enum-values': 'error', '@typescript-eslint/no-dynamic-delete': 'error', + '@typescript-eslint/no-empty-object-type': 'error', '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/no-extra-non-null-assertion': 'error', '@typescript-eslint/no-extraneous-class': 'error', From 173251c2f585d7c6a4eea25f8fb6b9fcc94f17f8 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Tue, 23 Apr 2024 17:10:39 -0400 Subject: [PATCH 02/21] Mention no-props --- packages/eslint-plugin/docs/rules/no-empty-object-type.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx b/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx index ffb2cb76324e..915a7cfe6838 100644 --- a/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx +++ b/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx @@ -20,6 +20,7 @@ Often, developers writing `{}` actually mean either: - `object`: representing any _object_ value - `unknown`: representing any value _other than `null` and `undefined`_ +- An object with no properties: which can't easily be represented in TypeScript's structural type system To avoid confusion around the `{}` type allowing non-object values, this rule bans usage of the `{}` type. From b83cb509023b2f27b5a114d1de56812ee499cff6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Tue, 23 Apr 2024 17:48:44 -0400 Subject: [PATCH 03/21] Update packages/eslint-plugin/docs/rules/no-empty-object-type.mdx Co-authored-by: Kirk Waiblinger <53019676+kirkwaiblinger@users.noreply.github.com> --- packages/eslint-plugin/docs/rules/no-empty-object-type.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx b/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx index 915a7cfe6838..a542ac76d1cb 100644 --- a/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx +++ b/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx @@ -19,7 +19,7 @@ let anyNonNullishValue: {} = 'Intentionally allowed by TypeScript.'; Often, developers writing `{}` actually mean either: - `object`: representing any _object_ value -- `unknown`: representing any value _other than `null` and `undefined`_ +- `NonNullable`: representing any value _other than `null` and `undefined`_ - An object with no properties: which can't easily be represented in TypeScript's structural type system To avoid confusion around the `{}` type allowing non-object values, this rule bans usage of the `{}` type. From 39870f1902ee8acaebb44a281637bb385176af6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Tue, 23 Apr 2024 17:53:55 -0400 Subject: [PATCH 04/21] Update packages/eslint-plugin/docs/rules/no-empty-object-type.mdx Co-authored-by: Kirk Waiblinger <53019676+kirkwaiblinger@users.noreply.github.com> --- packages/eslint-plugin/docs/rules/no-empty-object-type.mdx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx b/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx index a542ac76d1cb..120439df4448 100644 --- a/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx +++ b/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx @@ -19,7 +19,8 @@ let anyNonNullishValue: {} = 'Intentionally allowed by TypeScript.'; Often, developers writing `{}` actually mean either: - `object`: representing any _object_ value -- `NonNullable`: representing any value _other than `null` and `undefined`_ +- `NonNullable`: representing any value _other than `null` and `undefined`_. In most contexts this is the same as `{}`, but more explicit +- `unknown`: representing any value at all, including `null` and `undefined` - An object with no properties: which can't easily be represented in TypeScript's structural type system To avoid confusion around the `{}` type allowing non-object values, this rule bans usage of the `{}` type. From f90ab3f41c332af2c52f1398c4de702110b1b529 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Wed, 24 Apr 2024 08:39:14 -0400 Subject: [PATCH 05/21] Allow in intersections, and touch up docs per suggestions --- .../docs/rules/no-empty-object-type.mdx | 13 ++++++-- .../src/rules/no-empty-object-type.ts | 10 ++++++- .../tests/rules/no-empty-object-type.test.ts | 30 +++++++++++++++++++ 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx b/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx index 120439df4448..7e7e6db1c12b 100644 --- a/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx +++ b/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx @@ -19,16 +19,23 @@ let anyNonNullishValue: {} = 'Intentionally allowed by TypeScript.'; Often, developers writing `{}` actually mean either: - `object`: representing any _object_ value -- `NonNullable`: representing any value _other than `null` and `undefined`_. In most contexts this is the same as `{}`, but more explicit - `unknown`: representing any value at all, including `null` and `undefined` - An object with no properties: which can't easily be represented in TypeScript's structural type system -To avoid confusion around the `{}` type allowing non-object values, this rule bans usage of the `{}` type. +In other words, the "empty object" type `{}` really means _"any value that is defined"_. +That includes arrays, class instances, functions, and primitives such as `string` and `symbol`. + +To avoid confusion around the `{}` type allowing any _non-nullish value_, this rule bans usage of the `{}` type. :::tip -If you do have a use case for an API allowing any _non-nullish value_, you can always use an [ESLint disable comment](https://eslint.org/docs/latest/use/configure/rules#using-configuration-comments-1) or [disable the rule in your ESLint config](https://eslint.org/docs/latest/use/configure/rules#using-configuration-files-1). +If you do have a use case for an API allowing `{}`, you can always use an [ESLint disable comment](https://eslint.org/docs/latest/use/configure/rules#using-configuration-comments-1) or [disable the rule in your ESLint config](https://eslint.org/docs/latest/use/configure/rules#using-configuration-files-1). ::: +Note that this rule does not report: + +- `{}` as a type constituent in an intersection type (e.g. `type NonNullable = T & unknown`), as this is useful in type system operations. +- `{}` as an empty function body with a `void` return type (e.g. `() => {}`), as this is a function body, not a type. + ## Examples diff --git a/packages/eslint-plugin/src/rules/no-empty-object-type.ts b/packages/eslint-plugin/src/rules/no-empty-object-type.ts index d4d784477b91..b6c70355fbcc 100644 --- a/packages/eslint-plugin/src/rules/no-empty-object-type.ts +++ b/packages/eslint-plugin/src/rules/no-empty-object-type.ts @@ -1,4 +1,5 @@ import type { TSESLint } from '@typescript-eslint/utils'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule } from '../util'; @@ -26,7 +27,14 @@ export default createRule({ defaultOptions: [], create(context) { return { - 'TSTypeLiteral[members.length=0]'(node): void { + TSTypeLiteral(node): void { + if ( + node.members.length || + node.parent.type === AST_NODE_TYPES.TSIntersectionType + ) { + return; + } + context.report({ messageId: 'banEmptyObjectType', node, diff --git a/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts b/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts index fef527edaba3..cd2e31eadead 100644 --- a/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts +++ b/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts @@ -11,6 +11,7 @@ ruleTester.run('no-empty-object-type', rule, { 'let value: object;', 'let value: Object;', 'let value: { inner: true };', + 'type MyNonNullable = T & {};', ], invalid: [ { @@ -81,5 +82,34 @@ let value: Record; }, ], }, + { + code: 'type MyUnion = T | {};', + errors: [ + { + column: 23, + line: 1, + endColumn: 25, + endLine: 1, + messageId: 'banEmptyObjectType', + suggestions: [ + { + data: { replacement: 'object' }, + messageId: 'replaceEmptyObjectType', + output: 'type MyUnion = T | object;', + }, + { + data: { replacement: 'unknown' }, + messageId: 'replaceEmptyObjectType', + output: 'type MyUnion = T | unknown;', + }, + { + data: { replacement: 'Record' }, + messageId: 'replaceEmptyObjectType', + output: 'type MyUnion = T | Record;', + }, + ], + }, + ], + }, ], }); From 0a5c2bd2c3bd75803e84f5babbdfbdc9f690a871 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Wed, 24 Apr 2024 16:03:31 -0400 Subject: [PATCH 06/21] Update packages/eslint-plugin/docs/rules/no-empty-object-type.mdx Co-authored-by: Joshua Chen --- packages/eslint-plugin/docs/rules/no-empty-object-type.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx b/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx index 7e7e6db1c12b..b5d91fe0a115 100644 --- a/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx +++ b/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx @@ -33,7 +33,7 @@ If you do have a use case for an API allowing `{}`, you can always use an [ESLin Note that this rule does not report: -- `{}` as a type constituent in an intersection type (e.g. `type NonNullable = T & unknown`), as this is useful in type system operations. +- `{}` as a type constituent in an intersection type (e.g. `type NonNullable = T & {}`), as this is useful in type system operations. - `{}` as an empty function body with a `void` return type (e.g. `() => {}`), as this is a function body, not a type. ## Examples From c01f10e15eaa4bdc9b91a2f50d4e1b1c92eba8f6 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Thu, 25 Apr 2024 15:44:19 -0400 Subject: [PATCH 07/21] Trimming --- packages/eslint-plugin/docs/rules/ban-types.mdx | 4 ++++ packages/eslint-plugin/docs/rules/no-empty-object-type.mdx | 5 +---- packages/eslint-plugin/src/rules/no-empty-object-type.ts | 1 - 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/ban-types.mdx b/packages/eslint-plugin/docs/rules/ban-types.mdx index 98ce57edd719..ff547f7245fe 100644 --- a/packages/eslint-plugin/docs/rules/ban-types.mdx +++ b/packages/eslint-plugin/docs/rules/ban-types.mdx @@ -127,3 +127,7 @@ Example configuration: If your project is a rare one that intentionally deals with the class equivalents of primitives, it might not be worthwhile to enable the default `ban-types` options. 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. + +## Related To + +- [`no-empty-object-type`](./no-empty-object-type.mdx) diff --git a/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx b/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx index b5d91fe0a115..ab5f523c7ab3 100644 --- a/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx +++ b/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx @@ -31,10 +31,7 @@ To avoid confusion around the `{}` type allowing any _non-nullish value_, this r If you do have a use case for an API allowing `{}`, you can always use an [ESLint disable comment](https://eslint.org/docs/latest/use/configure/rules#using-configuration-comments-1) or [disable the rule in your ESLint config](https://eslint.org/docs/latest/use/configure/rules#using-configuration-files-1). ::: -Note that this rule does not report: - -- `{}` as a type constituent in an intersection type (e.g. `type NonNullable = T & {}`), as this is useful in type system operations. -- `{}` as an empty function body with a `void` return type (e.g. `() => {}`), as this is a function body, not a type. +Note that this rule does not report on `{}` as a type constituent in an intersection type (e.g. `type NonNullable = T & {}`), as this is useful in type system operations. ## Examples diff --git a/packages/eslint-plugin/src/rules/no-empty-object-type.ts b/packages/eslint-plugin/src/rules/no-empty-object-type.ts index b6c70355fbcc..8879fe0e659a 100644 --- a/packages/eslint-plugin/src/rules/no-empty-object-type.ts +++ b/packages/eslint-plugin/src/rules/no-empty-object-type.ts @@ -18,7 +18,6 @@ export default createRule({ "- If that's what you want, disable this lint rule with an inline comment or in your ESLint config.", '- If you want a type meaning "any object", you probably want `object` instead.', '- If you want a type meaning "any value", you probably want `unknown` instead.', - '- If you want a type meaning "empty object", you probably want `Record` instead.', ].join('\n'), replaceEmptyObjectType: 'Replace `{}` with `{{replacement}}`.', }, From f021d26e9d701b26026258f4a587d4f485b68c60 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Thu, 25 Apr 2024 16:21:16 -0400 Subject: [PATCH 08/21] Update snapshots --- .../docs-eslint-output-snapshots/no-empty-object-type.shot | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-empty-object-type.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-empty-object-type.shot index 1b1eadc79d09..e5ebbc3cd2ef 100644 --- a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-empty-object-type.shot +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-empty-object-type.shot @@ -8,19 +8,16 @@ let anyObject: {}; - If that's what you want, disable this lint rule with an inline comment or in your ESLint config. - If you want a type meaning "any object", you probably want \`object\` instead. - If you want a type meaning "any value", you probably want \`unknown\` instead. - - If you want a type meaning "empty object", you probably want \`Record\` instead. let anyValue: {}; ~~ The \`{}\` ("empty object") type allows any non-nullish value, including literals like \`0\` and \`""\`. - If that's what you want, disable this lint rule with an inline comment or in your ESLint config. - If you want a type meaning "any object", you probably want \`object\` instead. - If you want a type meaning "any value", you probably want \`unknown\` instead. - - If you want a type meaning "empty object", you probably want \`Record\` instead. let emptyObject: {}; ~~ The \`{}\` ("empty object") type allows any non-nullish value, including literals like \`0\` and \`""\`. - If that's what you want, disable this lint rule with an inline comment or in your ESLint config. - If you want a type meaning "any object", you probably want \`object\` instead. - If you want a type meaning "any value", you probably want \`unknown\` instead. - - If you want a type meaning "empty object", you probably want \`Record\` instead. " `; From ca7fb0fe6a0e0654856bb3b25029adafaa405c03 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Fri, 26 Apr 2024 07:29:34 -0400 Subject: [PATCH 09/21] Finish removing Record --- .../docs/rules/no-empty-object-type.mdx | 3 --- .../src/rules/no-empty-object-type.ts | 14 ++++++-------- .../tests/rules/no-empty-object-type.test.ts | 17 ----------------- 3 files changed, 6 insertions(+), 28 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx b/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx index ab5f523c7ab3..125b913130f9 100644 --- a/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx +++ b/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx @@ -20,7 +20,6 @@ Often, developers writing `{}` actually mean either: - `object`: representing any _object_ value - `unknown`: representing any value at all, including `null` and `undefined` -- An object with no properties: which can't easily be represented in TypeScript's structural type system In other words, the "empty object" type `{}` really means _"any value that is defined"_. That includes arrays, class instances, functions, and primitives such as `string` and `symbol`. @@ -41,7 +40,6 @@ Note that this rule does not report on `{}` as a type constituent in an intersec ```ts let anyObject: {}; let anyValue: {}; -let emptyObject: {}; ``` @@ -50,7 +48,6 @@ let emptyObject: {}; ```ts let anyObject: object; let anyValue: unknown; -let emptyObject: Record; ``` diff --git a/packages/eslint-plugin/src/rules/no-empty-object-type.ts b/packages/eslint-plugin/src/rules/no-empty-object-type.ts index 8879fe0e659a..c68c2e3d21e8 100644 --- a/packages/eslint-plugin/src/rules/no-empty-object-type.ts +++ b/packages/eslint-plugin/src/rules/no-empty-object-type.ts @@ -37,14 +37,12 @@ export default createRule({ context.report({ messageId: 'banEmptyObjectType', node, - suggest: ['object', 'unknown', 'Record'].map( - replacement => ({ - data: { replacement }, - messageId: 'replaceEmptyObjectType', - fix: (fixer): TSESLint.RuleFix => - fixer.replaceText(node, replacement), - }), - ), + suggest: ['object', 'unknown'].map(replacement => ({ + data: { replacement }, + messageId: 'replaceEmptyObjectType', + fix: (fixer): TSESLint.RuleFix => + fixer.replaceText(node, replacement), + })), }); }, }; diff --git a/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts b/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts index cd2e31eadead..5c9aa877e9f8 100644 --- a/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts +++ b/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts @@ -34,11 +34,6 @@ ruleTester.run('no-empty-object-type', rule, { messageId: 'replaceEmptyObjectType', output: 'let value: unknown;', }, - { - data: { replacement: 'Record' }, - messageId: 'replaceEmptyObjectType', - output: 'let value: Record;', - }, ], }, ], @@ -69,13 +64,6 @@ let value: object; messageId: 'replaceEmptyObjectType', output: ` let value: unknown; - `, - }, - { - data: { replacement: 'Record' }, - messageId: 'replaceEmptyObjectType', - output: ` -let value: Record; `, }, ], @@ -102,11 +90,6 @@ let value: Record; messageId: 'replaceEmptyObjectType', output: 'type MyUnion = T | unknown;', }, - { - data: { replacement: 'Record' }, - messageId: 'replaceEmptyObjectType', - output: 'type MyUnion = T | Record;', - }, ], }, ], From fad7a5e42eca9b3fcff91d9d9e57f0bf1bdcf56b Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Fri, 26 Apr 2024 07:33:13 -0400 Subject: [PATCH 10/21] Correct phrasing on Object --- packages/eslint-plugin/docs/rules/ban-types.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/ban-types.mdx b/packages/eslint-plugin/docs/rules/ban-types.mdx index ff547f7245fe..b52ae22df7d3 100644 --- a/packages/eslint-plugin/docs/rules/ban-types.mdx +++ b/packages/eslint-plugin/docs/rules/ban-types.mdx @@ -64,7 +64,7 @@ const capitalObj: { a: string } = { a: 'string' }; The default options provide a set of "best practices", intended to provide safety and standardization in your codebase: -- Don't use the upper-case primitive types, you should use the lower-case types for consistency. +- Don't use the upper-case primitive types or `Object`, you should use the lower-case types for consistency. - Avoid the `Function` type, as it provides little safety for the following reasons: - It provides no type safety when calling the value, which means it's easy to provide the wrong arguments. - It accepts class declarations, which will fail when called, as they are called without the `new` keyword. From e8a2d7fb5d59be94e7febf24d2b78facf675a8f3 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Tue, 30 Apr 2024 02:01:20 -0400 Subject: [PATCH 11/21] Merge with no-empty-interface --- .../docs/rules/no-empty-interface.mdx | 6 + .../docs/rules/no-empty-object-type.mdx | 65 ++- packages/eslint-plugin/src/configs/all.ts | 1 - .../src/configs/stylistic-type-checked.ts | 1 - .../eslint-plugin/src/configs/stylistic.ts | 1 - .../src/rules/no-empty-interface.ts | 2 +- .../src/rules/no-empty-object-type.ts | 166 +++++-- .../no-empty-object-type.shot | 51 ++- .../tests/rules/no-empty-object-type.test.ts | 411 +++++++++++++++++- .../no-empty-object-type.shot | 26 +- packages/typescript-eslint/src/configs/all.ts | 1 - .../src/configs/stylistic-type-checked.ts | 1 - .../src/configs/stylistic.ts | 1 - 13 files changed, 685 insertions(+), 48 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-empty-interface.mdx b/packages/eslint-plugin/docs/rules/no-empty-interface.mdx index aec497481242..733d8b39ebe6 100644 --- a/packages/eslint-plugin/docs/rules/no-empty-interface.mdx +++ b/packages/eslint-plugin/docs/rules/no-empty-interface.mdx @@ -9,6 +9,12 @@ import TabItem from '@theme/TabItem'; > > See **https://typescript-eslint.io/rules/no-empty-interface** for documentation. +:::danger Deprecated + +This rule has been deprecated in favour of the more comprehensive [`@typescript-eslint/no-empty-object-type`](./no-empty-object-type.mdx) rule. + +::: + An empty interface in TypeScript does very little: any non-nullable value is assignable to `{}`. Using an empty interface is often a sign of programmer error, such as misunderstanding the concept of `{}` or forgetting to fill in fields. diff --git a/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx b/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx index 125b913130f9..bf1852a0b756 100644 --- a/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx +++ b/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx @@ -25,12 +25,16 @@ In other words, the "empty object" type `{}` really means _"any value that is de That includes arrays, class instances, functions, and primitives such as `string` and `symbol`. To avoid confusion around the `{}` type allowing any _non-nullish value_, this rule bans usage of the `{}` type. +That includes interfaces and object type aliases with no fields. :::tip If you do have a use case for an API allowing `{}`, you can always use an [ESLint disable comment](https://eslint.org/docs/latest/use/configure/rules#using-configuration-comments-1) or [disable the rule in your ESLint config](https://eslint.org/docs/latest/use/configure/rules#using-configuration-files-1). ::: -Note that this rule does not report on `{}` as a type constituent in an intersection type (e.g. `type NonNullable = T & {}`), as this is useful in type system operations. +Note that this rule does not report on: + +- `{}` as a type constituent in an intersection type (e.g. `type NonNullable = T & {}`), as this can be useful in type system operations. +- Interfaces that extend from multiple other interfaces. ## Examples @@ -40,6 +44,12 @@ Note that this rule does not report on `{}` as a type constituent in an intersec ```ts let anyObject: {}; let anyValue: {}; + +interface AnyObjectA {} +interface AnyValueA {} + +type AnyObjectB = {}; +type AnyValueB = {}; ``` @@ -48,11 +58,60 @@ let anyValue: {}; ```ts let anyObject: object; let anyValue: unknown; + +type AnyObjectA = object; +type AnyValueA = unknown; + +type AnyObjectB = object; +type AnyValueB = unknown; + +let objectWith: { property: boolean }; + +interface InterfaceWith { + property: boolean; +} + +type TypeWith = { property: boolean }; ``` +## Options + +By default, this rule flags both interfaces and object types. + +:::warning +We strongly recommend not using either option's `'always'`. +The "empty object" type is a common source of confusion for even experienced TypeScript developers. +Consider using `object` or `unknown` as a type instead. +::: + +### `allowInterfaces` + +Whether to allow empty interfaces, as one of: + +- `'always'`: to always allow interfaces with no fields +- `'never'` _(default)_: to never allow interfaces with no fields +- `'with-single-extends'`: to allow empty interfaces that `extend` from a single base interface + +Examples of **correct** code for this rule with `{ allowInterfaces: 'with-single-extends' }`: + +```ts option='{ "allowInterfaces": "with-single-extends" }' showPlaygroundButton +interface Base { + value: boolean; +} + +interface Derived extends Base {} +``` + +### `allowObjectTypes` + +Whether to allow empty object type literals, as one of: + +- `'always'`: to always allow object type literals with no fields +- `'never'` _(default)_: to never allow object type literals with no fields + ## When Not To Use It If your code commonly needs to represent the _"any non-nullish value"_ type, this rule may not be for you. @@ -62,7 +121,3 @@ Projects that extensively use type operations such as conditional types and mapp - [Enhancement: [ban-types] Split the {} ban into a separate, better-phrased rule](https://github.com/typescript-eslint/typescript-eslint/issues/8700) - [The Empty Object Type in TypeScript](https://www.totaltypescript.com/the-empty-object-type-in-typescript) - -## Related To - -- [`no-empty-interface`](./no-empty-interface.mdx) diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index a9eb685a9c3a..0ac9c3653d97 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -54,7 +54,6 @@ export = { '@typescript-eslint/no-dynamic-delete': 'error', 'no-empty-function': 'off', '@typescript-eslint/no-empty-function': 'error', - '@typescript-eslint/no-empty-interface': 'error', '@typescript-eslint/no-empty-object-type': 'error', '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/no-extra-non-null-assertion': 'error', diff --git a/packages/eslint-plugin/src/configs/stylistic-type-checked.ts b/packages/eslint-plugin/src/configs/stylistic-type-checked.ts index 0bb075e5c8f2..3766c7f5695c 100644 --- a/packages/eslint-plugin/src/configs/stylistic-type-checked.ts +++ b/packages/eslint-plugin/src/configs/stylistic-type-checked.ts @@ -23,7 +23,6 @@ export = { '@typescript-eslint/no-confusing-non-null-assertion': 'error', 'no-empty-function': 'off', '@typescript-eslint/no-empty-function': 'error', - '@typescript-eslint/no-empty-interface': 'error', '@typescript-eslint/no-inferrable-types': 'error', '@typescript-eslint/non-nullable-type-assertion-style': 'error', '@typescript-eslint/prefer-for-of': 'error', diff --git a/packages/eslint-plugin/src/configs/stylistic.ts b/packages/eslint-plugin/src/configs/stylistic.ts index 74f2586dd78b..d9ac6faf9d9c 100644 --- a/packages/eslint-plugin/src/configs/stylistic.ts +++ b/packages/eslint-plugin/src/configs/stylistic.ts @@ -21,7 +21,6 @@ export = { '@typescript-eslint/no-confusing-non-null-assertion': 'error', 'no-empty-function': 'off', '@typescript-eslint/no-empty-function': 'error', - '@typescript-eslint/no-empty-interface': 'error', '@typescript-eslint/no-inferrable-types': 'error', '@typescript-eslint/prefer-for-of': 'error', '@typescript-eslint/prefer-function-type': 'error', diff --git a/packages/eslint-plugin/src/rules/no-empty-interface.ts b/packages/eslint-plugin/src/rules/no-empty-interface.ts index 674b86d44bf9..b3ada33093f3 100644 --- a/packages/eslint-plugin/src/rules/no-empty-interface.ts +++ b/packages/eslint-plugin/src/rules/no-empty-interface.ts @@ -17,8 +17,8 @@ export default createRule({ type: 'suggestion', docs: { description: 'Disallow the declaration of empty interfaces', - recommended: 'stylistic', }, + deprecated: true, fixable: 'code', hasSuggestions: true, messages: { diff --git a/packages/eslint-plugin/src/rules/no-empty-object-type.ts b/packages/eslint-plugin/src/rules/no-empty-object-type.ts index c68c2e3d21e8..15b9ac33adab 100644 --- a/packages/eslint-plugin/src/rules/no-empty-object-type.ts +++ b/packages/eslint-plugin/src/rules/no-empty-object-type.ts @@ -3,7 +3,25 @@ import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule } from '../util'; -export default createRule({ +export type AllowInterfaces = 'always' | 'never' | 'with-single-extends'; + +export type AllowObjectTypes = 'always' | 'never'; + +export type Options = [ + { + allowInterfaces?: AllowInterfaces; + allowObjectTypes?: AllowObjectTypes; + }, +]; + +export type MessageIds = + | 'noEmpty' + | 'noEmptyInterfaceWithSuper' + | 'replaceEmptyInterface' + | 'replaceEmptyInterfaceWithSuper' + | 'replaceEmptyObjectType'; + +export default createRule({ name: 'no-empty-object-type', meta: { type: 'suggestion', @@ -13,38 +31,136 @@ export default createRule({ }, hasSuggestions: true, messages: { - banEmptyObjectType: [ + noEmpty: [ 'The `{}` ("empty object") type allows any non-nullish value, including literals like `0` and `""`.', - "- If that's what you want, disable this lint rule with an inline comment or in your ESLint config.", + "- If that's what you want, disable this lint rule with an inline comment or configure the '{{ option }}' rule option.", '- If you want a type meaning "any object", you probably want `object` instead.', '- If you want a type meaning "any value", you probably want `unknown` instead.', ].join('\n'), + noEmptyInterfaceWithSuper: + 'An interface declaring no members is equivalent to its supertype.', + replaceEmptyInterface: 'Replace empty interface with `{{replacement}}`.', + replaceEmptyInterfaceWithSuper: + 'Replace empty interface with a type alias.', replaceEmptyObjectType: 'Replace `{}` with `{{replacement}}`.', }, - schema: [], + schema: [ + { + type: 'object', + additionalProperties: false, + properties: { + allowInterfaces: { + enum: ['always', 'never', 'with-single-extends'], + type: 'string', + }, + allowObjectTypes: { + enum: ['always', 'never'], + type: 'string', + }, + }, + }, + ], }, - defaultOptions: [], - create(context) { + defaultOptions: [ + { + allowInterfaces: 'never', + allowObjectTypes: 'never', + }, + ], + create(context, [{ allowInterfaces, allowObjectTypes }]) { return { - TSTypeLiteral(node): void { - if ( - node.members.length || - node.parent.type === AST_NODE_TYPES.TSIntersectionType - ) { - return; - } - - context.report({ - messageId: 'banEmptyObjectType', - node, - suggest: ['object', 'unknown'].map(replacement => ({ - data: { replacement }, - messageId: 'replaceEmptyObjectType', - fix: (fixer): TSESLint.RuleFix => - fixer.replaceText(node, replacement), - })), - }); - }, + ...(allowInterfaces !== 'always' && { + TSInterfaceDeclaration(node): void { + const extend = node.extends; + if ( + node.body.body.length !== 0 || + (extend.length === 1 && + allowInterfaces === 'with-single-extends') || + extend.length > 1 + ) { + return; + } + + const scope = context.sourceCode.getScope(node); + + const mergedWithClassDeclaration = scope.set + .get(node.id.name) + ?.defs.some( + def => def.node.type === AST_NODE_TYPES.ClassDeclaration, + ); + + if (extend.length === 0) { + context.report({ + data: { option: 'allowInterfaces' }, + node: node.id, + messageId: 'noEmpty', + ...(!mergedWithClassDeclaration && { + suggest: ['object', 'unknown'].map(replacement => ({ + fix(fixer): TSESLint.RuleFix { + const id = context.sourceCode.getText(node.id); + const typeParam = node.typeParameters + ? context.sourceCode.getText(node.typeParameters) + : ''; + + return fixer.replaceText( + node, + `type ${id}${typeParam} = ${replacement}`, + ); + }, + messageId: 'replaceEmptyInterface', + })), + }), + }); + return; + } + + context.report({ + node: node.id, + messageId: 'noEmptyInterfaceWithSuper', + ...(!mergedWithClassDeclaration && { + suggest: [ + { + fix(fixer): TSESLint.RuleFix { + const extended = context.sourceCode.getText(extend[0]); + const id = context.sourceCode.getText(node.id); + const typeParam = node.typeParameters + ? context.sourceCode.getText(node.typeParameters) + : ''; + + return fixer.replaceText( + node, + `type ${id}${typeParam} = ${extended}`, + ); + }, + messageId: 'replaceEmptyInterfaceWithSuper', + }, + ], + }), + }); + }, + }), + ...(allowObjectTypes !== 'always' && { + TSTypeLiteral(node): void { + if ( + node.members.length || + node.parent.type === AST_NODE_TYPES.TSIntersectionType + ) { + return; + } + + context.report({ + data: { option: 'allowObjectTypes' }, + messageId: 'noEmpty', + node, + suggest: ['object', 'unknown'].map(replacement => ({ + data: { replacement }, + messageId: 'replaceEmptyObjectType', + fix: (fixer): TSESLint.RuleFix => + fixer.replaceText(node, replacement), + })), + }); + }, + }), }; }, }); diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-empty-object-type.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-empty-object-type.shot index e5ebbc3cd2ef..4dba11690551 100644 --- a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-empty-object-type.shot +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-empty-object-type.shot @@ -5,17 +5,34 @@ exports[`Validating rule docs no-empty-object-type.mdx code examples ESLint outp let anyObject: {}; ~~ The \`{}\` ("empty object") type allows any non-nullish value, including literals like \`0\` and \`""\`. - - If that's what you want, disable this lint rule with an inline comment or in your ESLint config. + - If that's what you want, disable this lint rule with an inline comment or configure the 'allowObjectTypes' rule option. - If you want a type meaning "any object", you probably want \`object\` instead. - If you want a type meaning "any value", you probably want \`unknown\` instead. let anyValue: {}; ~~ The \`{}\` ("empty object") type allows any non-nullish value, including literals like \`0\` and \`""\`. - - If that's what you want, disable this lint rule with an inline comment or in your ESLint config. + - If that's what you want, disable this lint rule with an inline comment or configure the 'allowObjectTypes' rule option. - If you want a type meaning "any object", you probably want \`object\` instead. - If you want a type meaning "any value", you probably want \`unknown\` instead. -let emptyObject: {}; + +interface AnyObjectA {} + ~~~~~~~~~~ The \`{}\` ("empty object") type allows any non-nullish value, including literals like \`0\` and \`""\`. + - If that's what you want, disable this lint rule with an inline comment or configure the 'allowInterfaces' rule option. + - If you want a type meaning "any object", you probably want \`object\` instead. + - If you want a type meaning "any value", you probably want \`unknown\` instead. +interface AnyValueA {} + ~~~~~~~~~ The \`{}\` ("empty object") type allows any non-nullish value, including literals like \`0\` and \`""\`. + - If that's what you want, disable this lint rule with an inline comment or configure the 'allowInterfaces' rule option. + - If you want a type meaning "any object", you probably want \`object\` instead. + - If you want a type meaning "any value", you probably want \`unknown\` instead. + +type AnyObjectB = {}; + ~~ The \`{}\` ("empty object") type allows any non-nullish value, including literals like \`0\` and \`""\`. + - If that's what you want, disable this lint rule with an inline comment or configure the 'allowObjectTypes' rule option. + - If you want a type meaning "any object", you probably want \`object\` instead. + - If you want a type meaning "any value", you probably want \`unknown\` instead. +type AnyValueB = {}; ~~ The \`{}\` ("empty object") type allows any non-nullish value, including literals like \`0\` and \`""\`. - - If that's what you want, disable this lint rule with an inline comment or in your ESLint config. + - If that's what you want, disable this lint rule with an inline comment or configure the 'allowObjectTypes' rule option. - If you want a type meaning "any object", you probably want \`object\` instead. - If you want a type meaning "any value", you probably want \`unknown\` instead. " @@ -26,6 +43,30 @@ exports[`Validating rule docs no-empty-object-type.mdx code examples ESLint outp let anyObject: object; let anyValue: unknown; -let emptyObject: Record; + +type AnyObjectA = object; +type AnyValueA = unknown; + +type AnyObjectB = object; +type AnyValueB = unknown; + +let objectWith: { property: boolean }; + +interface InterfaceWith { + property: boolean; +} + +type TypeWith = { property: boolean }; +" +`; + +exports[`Validating rule docs no-empty-object-type.mdx code examples ESLint output 3`] = ` +"Options: { "allowInterfaces": "with-single-extends" } + +interface Base { + value: boolean; +} + +interface Derived extends Base {} " `; diff --git a/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts b/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts index 5c9aa877e9f8..cd4717959c6d 100644 --- a/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts +++ b/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts @@ -8,12 +8,413 @@ const ruleTester = new RuleTester({ ruleTester.run('no-empty-object-type', rule, { valid: [ + ` +interface Base { + name: string; +} + `, + ` +interface Base { + name: string; +} + +interface Derived { + age: number; +} + +// valid because extending multiple interfaces can be used instead of a union type +interface Both extends Base, Derived {} + `, + { + code: 'interface Base {}', + options: [{ allowInterfaces: 'always' }], + }, + { + code: ` +interface Base { + name: string; +} + +interface Derived extends Base {} + `, + options: [{ allowInterfaces: 'with-single-extends' }], + }, + { + code: ` +interface Base { + props: string; +} + +interface Derived extends Base {} + +class Derived {} + `, + options: [{ allowInterfaces: 'with-single-extends' }], + }, 'let value: object;', 'let value: Object;', 'let value: { inner: true };', 'type MyNonNullable = T & {};', + { + code: 'type Base = {};', + options: [{ allowObjectTypes: 'always' }], + }, ], invalid: [ + { + code: 'interface Base {}', + errors: [ + { + data: { option: 'allowInterfaces' }, + messageId: 'noEmpty', + line: 1, + column: 11, + }, + ], + }, + { + code: 'interface Base {}', + errors: [ + { + data: { option: 'allowInterfaces' }, + messageId: 'noEmpty', + line: 1, + column: 11, + }, + ], + options: [{ allowInterfaces: 'never' }], + }, + { + code: ` +interface Base { + props: string; +} + +interface Derived extends Base {} + +class Other {} + `, + errors: [ + { + messageId: 'noEmptyInterfaceWithSuper', + line: 6, + column: 11, + suggestions: [ + { + messageId: 'replaceEmptyInterfaceWithSuper', + output: ` +interface Base { + props: string; +} + +type Derived = Base + +class Other {} + `, + }, + ], + }, + ], + }, + { + code: ` +interface Base { + props: string; +} + +interface Derived extends Base {} + +class Derived {} + `, + errors: [ + { + messageId: 'noEmptyInterfaceWithSuper', + line: 6, + column: 11, + }, + ], + }, + { + code: ` +interface Base { + props: string; +} + +interface Derived extends Base {} + +const derived = class Derived {}; + `, + errors: [ + { + messageId: 'noEmptyInterfaceWithSuper', + line: 6, + column: 11, + suggestions: [ + { + messageId: 'replaceEmptyInterfaceWithSuper', + output: ` +interface Base { + props: string; +} + +type Derived = Base + +const derived = class Derived {}; + `, + }, + ], + }, + ], + }, + { + code: ` +interface Base { + name: string; +} + +interface Derived extends Base {} + `, + errors: [ + { + messageId: 'noEmptyInterfaceWithSuper', + line: 6, + column: 11, + suggestions: [ + { + messageId: 'replaceEmptyInterfaceWithSuper', + output: ` +interface Base { + name: string; +} + +type Derived = Base + `, + }, + ], + }, + ], + }, + { + code: 'interface Base extends Array {}', + errors: [ + { + messageId: 'noEmptyInterfaceWithSuper', + line: 1, + column: 11, + suggestions: [ + { + messageId: 'replaceEmptyInterfaceWithSuper', + output: `type Base = Array`, + }, + ], + }, + ], + }, + { + code: 'interface Base extends Array {}', + errors: [ + { + messageId: 'noEmptyInterfaceWithSuper', + line: 1, + column: 11, + endColumn: 15, + suggestions: [ + { + messageId: 'replaceEmptyInterfaceWithSuper', + output: `type Base = Array`, + }, + ], + }, + { + data: { option: 'allowObjectTypes' }, + messageId: 'noEmpty', + line: 1, + column: 39, + endColumn: 41, + suggestions: [ + { + data: { replacement: 'object' }, + messageId: 'replaceEmptyObjectType', + output: `interface Base extends Array {}`, + }, + { + data: { replacement: 'unknown' }, + messageId: 'replaceEmptyObjectType', + output: `interface Base extends Array {}`, + }, + ], + }, + ], + }, + { + code: ` +interface Derived { + property: string; +} +interface Base extends Array {} + `, + errors: [ + { + messageId: 'noEmptyInterfaceWithSuper', + line: 5, + column: 11, + suggestions: [ + { + messageId: 'replaceEmptyInterfaceWithSuper', + output: ` +interface Derived { + property: string; +} +type Base = Array + `, + }, + ], + }, + ], + }, + { + code: ` +type R = Record; +interface Base extends R {} + `, + errors: [ + { + messageId: 'noEmptyInterfaceWithSuper', + line: 3, + column: 11, + suggestions: [ + { + messageId: 'replaceEmptyInterfaceWithSuper', + output: ` +type R = Record; +type Base = R + `, + }, + ], + }, + ], + }, + { + code: 'interface Base extends Derived {}', + errors: [ + { + messageId: 'noEmptyInterfaceWithSuper', + line: 1, + column: 11, + suggestions: [ + { + messageId: 'replaceEmptyInterfaceWithSuper', + output: `type Base = Derived`, + }, + ], + }, + ], + }, + { + filename: 'test.d.ts', + code: ` +declare namespace BaseAndDerived { + type Base = typeof base; + export interface Derived extends Base {} +} + `, + errors: [ + { + messageId: 'noEmptyInterfaceWithSuper', + line: 4, + column: 20, + endLine: 4, + endColumn: 27, + suggestions: [ + { + messageId: 'replaceEmptyInterfaceWithSuper', + output: ` +declare namespace BaseAndDerived { + type Base = typeof base; + export type Derived = Base +} + `, + }, + ], + }, + ], + }, + { + code: 'type Base = {};', + errors: [ + { + column: 13, + line: 1, + endColumn: 15, + endLine: 1, + data: { option: 'allowObjectTypes' }, + messageId: 'noEmpty', + suggestions: [ + { + data: { replacement: 'object' }, + messageId: 'replaceEmptyObjectType', + output: 'type Base = object;', + }, + { + data: { replacement: 'unknown' }, + messageId: 'replaceEmptyObjectType', + output: 'type Base = unknown;', + }, + ], + }, + ], + }, + { + code: 'type Base = {};', + errors: [ + { + column: 13, + line: 1, + endColumn: 15, + endLine: 1, + data: { option: 'allowObjectTypes' }, + messageId: 'noEmpty', + suggestions: [ + { + data: { replacement: 'object' }, + messageId: 'replaceEmptyObjectType', + output: 'type Base = object;', + }, + { + data: { replacement: 'unknown' }, + messageId: 'replaceEmptyObjectType', + output: 'type Base = unknown;', + }, + ], + }, + ], + options: [{ allowObjectTypes: 'never' }], + }, + { + code: 'let value: {};', + errors: [ + { + column: 12, + line: 1, + endColumn: 14, + endLine: 1, + data: { option: 'allowObjectTypes' }, + messageId: 'noEmpty', + suggestions: [ + { + data: { replacement: 'object' }, + messageId: 'replaceEmptyObjectType', + output: 'let value: object;', + }, + { + data: { replacement: 'unknown' }, + messageId: 'replaceEmptyObjectType', + output: 'let value: unknown;', + }, + ], + }, + ], + }, { code: 'let value: {};', errors: [ @@ -22,7 +423,8 @@ ruleTester.run('no-empty-object-type', rule, { line: 1, endColumn: 14, endLine: 1, - messageId: 'banEmptyObjectType', + data: { option: 'allowObjectTypes' }, + messageId: 'noEmpty', suggestions: [ { data: { replacement: 'object' }, @@ -37,6 +439,7 @@ ruleTester.run('no-empty-object-type', rule, { ], }, ], + options: [{ allowObjectTypes: 'never' }], }, { code: ` @@ -50,7 +453,8 @@ let value: { endLine: 4, column: 12, endColumn: 2, - messageId: 'banEmptyObjectType', + data: { option: 'allowObjectTypes' }, + messageId: 'noEmpty', suggestions: [ { data: { replacement: 'object' }, @@ -78,7 +482,8 @@ let value: unknown; line: 1, endColumn: 25, endLine: 1, - messageId: 'banEmptyObjectType', + data: { option: 'allowObjectTypes' }, + messageId: 'noEmpty', suggestions: [ { data: { replacement: 'object' }, diff --git a/packages/eslint-plugin/tests/schema-snapshots/no-empty-object-type.shot b/packages/eslint-plugin/tests/schema-snapshots/no-empty-object-type.shot index 8f658c6d922b..2542efd5441c 100644 --- a/packages/eslint-plugin/tests/schema-snapshots/no-empty-object-type.shot +++ b/packages/eslint-plugin/tests/schema-snapshots/no-empty-object-type.shot @@ -4,11 +4,31 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos " # SCHEMA: -[] +[ + { + "additionalProperties": false, + "properties": { + "allowInterfaces": { + "enum": ["always", "never", "with-single-extends"], + "type": "string" + }, + "allowObjectTypes": { + "enum": ["always", "never"], + "type": "string" + } + }, + "type": "object" + } +] # TYPES: -/** No options declared */ -type Options = [];" +type Options = [ + { + allowInterfaces?: 'always' | 'never' | 'with-single-extends'; + allowObjectTypes?: 'always' | 'never'; + }, +]; +" `; diff --git a/packages/typescript-eslint/src/configs/all.ts b/packages/typescript-eslint/src/configs/all.ts index 5341ff26fd21..01d4a56f9ee1 100644 --- a/packages/typescript-eslint/src/configs/all.ts +++ b/packages/typescript-eslint/src/configs/all.ts @@ -63,7 +63,6 @@ export default ( '@typescript-eslint/no-dynamic-delete': 'error', 'no-empty-function': 'off', '@typescript-eslint/no-empty-function': 'error', - '@typescript-eslint/no-empty-interface': 'error', '@typescript-eslint/no-empty-object-type': 'error', '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/no-extra-non-null-assertion': 'error', diff --git a/packages/typescript-eslint/src/configs/stylistic-type-checked.ts b/packages/typescript-eslint/src/configs/stylistic-type-checked.ts index 95b8c516fae5..63ef5a71d0f4 100644 --- a/packages/typescript-eslint/src/configs/stylistic-type-checked.ts +++ b/packages/typescript-eslint/src/configs/stylistic-type-checked.ts @@ -32,7 +32,6 @@ export default ( '@typescript-eslint/no-confusing-non-null-assertion': 'error', 'no-empty-function': 'off', '@typescript-eslint/no-empty-function': 'error', - '@typescript-eslint/no-empty-interface': 'error', '@typescript-eslint/no-inferrable-types': 'error', '@typescript-eslint/non-nullable-type-assertion-style': 'error', '@typescript-eslint/prefer-for-of': 'error', diff --git a/packages/typescript-eslint/src/configs/stylistic.ts b/packages/typescript-eslint/src/configs/stylistic.ts index 6a9b4cd306dd..6e12fe9de23a 100644 --- a/packages/typescript-eslint/src/configs/stylistic.ts +++ b/packages/typescript-eslint/src/configs/stylistic.ts @@ -30,7 +30,6 @@ export default ( '@typescript-eslint/no-confusing-non-null-assertion': 'error', 'no-empty-function': 'off', '@typescript-eslint/no-empty-function': 'error', - '@typescript-eslint/no-empty-interface': 'error', '@typescript-eslint/no-inferrable-types': 'error', '@typescript-eslint/prefer-for-of': 'error', '@typescript-eslint/prefer-function-type': 'error', From 3971d71103016652f7bba8dc00192259ced75980 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Tue, 30 Apr 2024 02:05:45 -0400 Subject: [PATCH 12/21] nit the tip --- packages/eslint-plugin/docs/rules/no-empty-object-type.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx b/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx index bf1852a0b756..dcb5dfd5fe39 100644 --- a/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx +++ b/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx @@ -28,7 +28,7 @@ To avoid confusion around the `{}` type allowing any _non-nullish value_, this r That includes interfaces and object type aliases with no fields. :::tip -If you do have a use case for an API allowing `{}`, you can always use an [ESLint disable comment](https://eslint.org/docs/latest/use/configure/rules#using-configuration-comments-1) or [disable the rule in your ESLint config](https://eslint.org/docs/latest/use/configure/rules#using-configuration-files-1). +If you do have a use case for an API allowing `{}`, you can always configure the [rule's options](#options), use an [ESLint disable comment](https://eslint.org/docs/latest/use/configure/rules#using-configuration-comments-1), or [disable the rule in your ESLint config](https://eslint.org/docs/latest/use/configure/rules#using-configuration-files-1). ::: Note that this rule does not report on: From d33a2460eb3a1932f645a8dce634d659118b0cf7 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Wed, 8 May 2024 08:36:34 -0700 Subject: [PATCH 13/21] Explicit report message for interfaces --- .../src/rules/no-empty-object-type.ts | 23 +++++++++++-------- .../tests/rules/no-empty-object-type.test.ts | 18 +++++++-------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-empty-object-type.ts b/packages/eslint-plugin/src/rules/no-empty-object-type.ts index 15b9ac33adab..456fd0ac9b79 100644 --- a/packages/eslint-plugin/src/rules/no-empty-object-type.ts +++ b/packages/eslint-plugin/src/rules/no-empty-object-type.ts @@ -15,12 +15,21 @@ export type Options = [ ]; export type MessageIds = - | 'noEmpty' + | 'noEmptyInterface' + | 'noEmptyObject' | 'noEmptyInterfaceWithSuper' | 'replaceEmptyInterface' | 'replaceEmptyInterfaceWithSuper' | 'replaceEmptyObjectType'; +const noEmptyMessage = (emptyType: string): string => + [ + `${emptyType} allows any non-nullish value, including literals like \`0\` and \`""\`.`, + "- If that's what you want, disable this lint rule with an inline comment or configure the '{{ option }}' rule option.", + '- If you want a type meaning "any object", you probably want `object` instead.', + '- If you want a type meaning "any value", you probably want `unknown` instead.', + ].join('\n'); + export default createRule({ name: 'no-empty-object-type', meta: { @@ -31,12 +40,8 @@ export default createRule({ }, hasSuggestions: true, messages: { - noEmpty: [ - 'The `{}` ("empty object") type allows any non-nullish value, including literals like `0` and `""`.', - "- If that's what you want, disable this lint rule with an inline comment or configure the '{{ option }}' rule option.", - '- If you want a type meaning "any object", you probably want `object` instead.', - '- If you want a type meaning "any value", you probably want `unknown` instead.', - ].join('\n'), + noEmptyInterface: noEmptyMessage('An empty interface declaration'), + noEmptyObject: noEmptyMessage('The `{}` ("empty object") type'), noEmptyInterfaceWithSuper: 'An interface declaring no members is equivalent to its supertype.', replaceEmptyInterface: 'Replace empty interface with `{{replacement}}`.', @@ -93,7 +98,7 @@ export default createRule({ context.report({ data: { option: 'allowInterfaces' }, node: node.id, - messageId: 'noEmpty', + messageId: 'noEmptyInterface', ...(!mergedWithClassDeclaration && { suggest: ['object', 'unknown'].map(replacement => ({ fix(fixer): TSESLint.RuleFix { @@ -150,7 +155,7 @@ export default createRule({ context.report({ data: { option: 'allowObjectTypes' }, - messageId: 'noEmpty', + messageId: 'noEmptyObject', node, suggest: ['object', 'unknown'].map(replacement => ({ data: { replacement }, diff --git a/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts b/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts index cd4717959c6d..68509ecdae2c 100644 --- a/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts +++ b/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts @@ -66,7 +66,7 @@ class Derived {} errors: [ { data: { option: 'allowInterfaces' }, - messageId: 'noEmpty', + messageId: 'noEmptyInterface', line: 1, column: 11, }, @@ -77,7 +77,7 @@ class Derived {} errors: [ { data: { option: 'allowInterfaces' }, - messageId: 'noEmpty', + messageId: 'noEmptyInterface', line: 1, column: 11, }, @@ -227,7 +227,7 @@ type Derived = Base }, { data: { option: 'allowObjectTypes' }, - messageId: 'noEmpty', + messageId: 'noEmptyObject', line: 1, column: 39, endColumn: 41, @@ -348,7 +348,7 @@ declare namespace BaseAndDerived { endColumn: 15, endLine: 1, data: { option: 'allowObjectTypes' }, - messageId: 'noEmpty', + messageId: 'noEmptyObject', suggestions: [ { data: { replacement: 'object' }, @@ -373,7 +373,7 @@ declare namespace BaseAndDerived { endColumn: 15, endLine: 1, data: { option: 'allowObjectTypes' }, - messageId: 'noEmpty', + messageId: 'noEmptyObject', suggestions: [ { data: { replacement: 'object' }, @@ -399,7 +399,7 @@ declare namespace BaseAndDerived { endColumn: 14, endLine: 1, data: { option: 'allowObjectTypes' }, - messageId: 'noEmpty', + messageId: 'noEmptyObject', suggestions: [ { data: { replacement: 'object' }, @@ -424,7 +424,7 @@ declare namespace BaseAndDerived { endColumn: 14, endLine: 1, data: { option: 'allowObjectTypes' }, - messageId: 'noEmpty', + messageId: 'noEmptyObject', suggestions: [ { data: { replacement: 'object' }, @@ -454,7 +454,7 @@ let value: { column: 12, endColumn: 2, data: { option: 'allowObjectTypes' }, - messageId: 'noEmpty', + messageId: 'noEmptyObject', suggestions: [ { data: { replacement: 'object' }, @@ -483,7 +483,7 @@ let value: unknown; endColumn: 25, endLine: 1, data: { option: 'allowObjectTypes' }, - messageId: 'noEmpty', + messageId: 'noEmptyObject', suggestions: [ { data: { replacement: 'object' }, From 28ed70dca5f1742960ce3bf0dee398029171aa92 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Wed, 8 May 2024 18:59:03 -0700 Subject: [PATCH 14/21] Add in-type-alias-with-name --- .../docs/rules/no-empty-object-type.mdx | 28 +++++++++++++++++-- .../src/rules/no-empty-object-type.ts | 13 +++++++-- .../tests/rules/no-empty-object-type.test.ts | 17 +++++++++++ 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx b/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx index dcb5dfd5fe39..85611ae64c2e 100644 --- a/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx +++ b/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx @@ -82,8 +82,8 @@ type TypeWith = { property: boolean }; By default, this rule flags both interfaces and object types. :::warning -We strongly recommend not using either option's `'always'`. -The "empty object" type is a common source of confusion for even experienced TypeScript developers. +We strongly recommend keeping both option to the default, `'never'`. +The "empty object" type is often confusing even for experienced TypeScript developers. Consider using `object` or `unknown` as a type instead. ::: @@ -110,8 +110,32 @@ interface Derived extends Base {} Whether to allow empty object type literals, as one of: - `'always'`: to always allow object type literals with no fields +- `'in-type-alias-with-name'`: to only allow object type literals as the type in a named object type alias - `'never'` _(default)_: to never allow object type literals with no fields +Example of code for this rule with `{ allowObjectTypes: 'in-type-alias-with-name' }`: + + + + +```ts option='{ "allowObjectTypes": "in-type-alias-with-name" }' showPlaygroundButton +type EmptyObjectProps = {}; + +declare function takesEmptyObject(value: {}): void; +``` + + + + +```ts option='{ "allowObjectTypes": "in-type-alias-with-name" }' showPlaygroundButton +type EmptyObjectProps = {} | null; + +declare function takesEmptyObject(value: object): void; +``` + + + + ## When Not To Use It If your code commonly needs to represent the _"any non-nullish value"_ type, this rule may not be for you. diff --git a/packages/eslint-plugin/src/rules/no-empty-object-type.ts b/packages/eslint-plugin/src/rules/no-empty-object-type.ts index 456fd0ac9b79..07f141a632cd 100644 --- a/packages/eslint-plugin/src/rules/no-empty-object-type.ts +++ b/packages/eslint-plugin/src/rules/no-empty-object-type.ts @@ -11,6 +11,7 @@ export type Options = [ { allowInterfaces?: AllowInterfaces; allowObjectTypes?: AllowObjectTypes; + allowInTypeAliasWithName?: boolean; }, ]; @@ -59,7 +60,7 @@ export default createRule({ type: 'string', }, allowObjectTypes: { - enum: ['always', 'never'], + enum: ['always', 'in-type-alias-with-name', 'never'], type: 'string', }, }, @@ -70,9 +71,13 @@ export default createRule({ { allowInterfaces: 'never', allowObjectTypes: 'never', + allowInTypeAliasWithName: false, }, ], - create(context, [{ allowInterfaces, allowObjectTypes }]) { + create( + context, + [{ allowInterfaces, allowInTypeAliasWithName, allowObjectTypes }], + ) { return { ...(allowInterfaces !== 'always' && { TSInterfaceDeclaration(node): void { @@ -148,7 +153,9 @@ export default createRule({ TSTypeLiteral(node): void { if ( node.members.length || - node.parent.type === AST_NODE_TYPES.TSIntersectionType + node.parent.type === AST_NODE_TYPES.TSIntersectionType || + (allowInTypeAliasWithName && + node.parent.type === AST_NODE_TYPES.TSTypeAliasDeclaration) ) { return; } diff --git a/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts b/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts index 68509ecdae2c..414f6f0d5d0b 100644 --- a/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts +++ b/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts @@ -59,6 +59,10 @@ class Derived {} code: 'type Base = {};', options: [{ allowObjectTypes: 'always' }], }, + { + code: 'type Base = {};', + options: [{ allowInTypeAliasWithName: true }], + }, ], invalid: [ { @@ -499,5 +503,18 @@ let value: unknown; }, ], }, + { + code: 'type Base = {} | null;', + errors: [ + { + column: 13, + line: 1, + endColumn: 15, + endLine: 1, + messageId: 'noEmptyObject', + }, + ], + options: [{ allowInTypeAliasWithName: true }], + }, ], }); From 9b6dd1a80bbceadecfe54733bf89058e8a34b92a Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Thu, 9 May 2024 08:02:04 -0700 Subject: [PATCH 15/21] Switched to more general allowWithName --- .../eslint-plugin/TSLINT_RULE_ALTERNATIVES.md | 4 +- .../docs/rules/no-empty-object-type.mdx | 22 ++++++---- .../src/rules/no-empty-object-type.ts | 24 +++++++---- .../tests/rules/no-empty-object-type.test.ts | 42 ++++++++++++++++++- packages/typescript-estree/src/parser.ts | 2 +- .../src/ts-estree/ts-nodes.ts | 4 +- packages/utils/src/json-schema.ts | 2 +- packages/utils/src/ts-eslint/Rule.ts | 2 +- 8 files changed, 76 insertions(+), 26 deletions(-) diff --git a/packages/eslint-plugin/TSLINT_RULE_ALTERNATIVES.md b/packages/eslint-plugin/TSLINT_RULE_ALTERNATIVES.md index 8276fabd9226..d6dd55f8b9e3 100644 --- a/packages/eslint-plugin/TSLINT_RULE_ALTERNATIVES.md +++ b/packages/eslint-plugin/TSLINT_RULE_ALTERNATIVES.md @@ -24,7 +24,7 @@ It lists all TSLint rules along side rules from the ESLint ecosystem that are th | [`member-access`] | ✅ | [`@typescript-eslint/explicit-member-accessibility`] | | [`member-ordering`] | ✅ | [`@typescript-eslint/member-ordering`] | | [`no-any`] | ✅ | [`@typescript-eslint/no-explicit-any`] | -| [`no-empty-interface`] | ✅ | [`@typescript-eslint/no-empty-interface`] | +| [`no-empty-interface`] | ✅ | [`@typescript-eslint/no-empty-object-type`] | | [`no-import-side-effect`] | 🔌 | [`import/no-unassigned-import`] | | [`no-inferrable-types`] | ✅ | [`@typescript-eslint/no-inferrable-types`] | | [`no-internal-module`] | ✅ | [`@typescript-eslint/prefer-namespace-keyword`] | @@ -604,7 +604,7 @@ Relevant plugins: [`chai-expect-keywords`](https://github.com/gavinaiken/eslint- [`@typescript-eslint/member-ordering`]: https://typescript-eslint.io/rules/member-ordering [`@typescript-eslint/method-signature-style`]: https://typescript-eslint.io/rules/method-signature-style [`@typescript-eslint/no-explicit-any`]: https://typescript-eslint.io/rules/no-explicit-any -[`@typescript-eslint/no-empty-interface`]: https://typescript-eslint.io/rules/no-empty-interface +[`@typescript-eslint/no-empty-object-type`]: https://typescript-eslint.io/rules/no-empty-object-type [`@typescript-eslint/no-implied-eval`]: https://typescript-eslint.io/rules/no-implied-eval [`@typescript-eslint/no-inferrable-types`]: https://typescript-eslint.io/rules/no-inferrable-types [`@typescript-eslint/prefer-namespace-keyword`]: https://typescript-eslint.io/rules/prefer-namespace-keyword diff --git a/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx b/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx index 85611ae64c2e..37e53aba56e8 100644 --- a/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx +++ b/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx @@ -82,7 +82,7 @@ type TypeWith = { property: boolean }; By default, this rule flags both interfaces and object types. :::warning -We strongly recommend keeping both option to the default, `'never'`. +We recommend keeping all options to the default and not allowing exceptions to this rule. The "empty object" type is often confusing even for experienced TypeScript developers. Consider using `object` or `unknown` as a type instead. ::: @@ -110,27 +110,31 @@ interface Derived extends Base {} Whether to allow empty object type literals, as one of: - `'always'`: to always allow object type literals with no fields -- `'in-type-alias-with-name'`: to only allow object type literals as the type in a named object type alias - `'never'` _(default)_: to never allow object type literals with no fields -Example of code for this rule with `{ allowObjectTypes: 'in-type-alias-with-name' }`: +### `allowWithName` + +A stringified regular expression to allow interfaces and object type aliases with the configured name. +This can be useful if your existing code style still includes a pattern of declaring empty types with `{}` instead of `object`. + +Examples of code for this rule with `{ allowWithName: 'Props$' }`: -```ts option='{ "allowObjectTypes": "in-type-alias-with-name" }' showPlaygroundButton -type EmptyObjectProps = {}; +```ts option='{ "allowInterfaces": "Props$" }' showPlaygroundButton +interface InterfaceValue {} -declare function takesEmptyObject(value: {}): void; +type TypeValue = {}; ``` -```ts option='{ "allowObjectTypes": "in-type-alias-with-name" }' showPlaygroundButton -type EmptyObjectProps = {} | null; +```ts option='{ "allowInterfaces": "Props$" }' showPlaygroundButton +interface InterfaceProps {} -declare function takesEmptyObject(value: object): void; +type TypeProps = {}; ``` diff --git a/packages/eslint-plugin/src/rules/no-empty-object-type.ts b/packages/eslint-plugin/src/rules/no-empty-object-type.ts index 07f141a632cd..47f3eb694c61 100644 --- a/packages/eslint-plugin/src/rules/no-empty-object-type.ts +++ b/packages/eslint-plugin/src/rules/no-empty-object-type.ts @@ -11,7 +11,7 @@ export type Options = [ { allowInterfaces?: AllowInterfaces; allowObjectTypes?: AllowObjectTypes; - allowInTypeAliasWithName?: boolean; + allowWithName?: string; }, ]; @@ -63,6 +63,9 @@ export default createRule({ enum: ['always', 'in-type-alias-with-name', 'never'], type: 'string', }, + allowWithName: { + type: 'string', + }, }, }, ], @@ -71,16 +74,20 @@ export default createRule({ { allowInterfaces: 'never', allowObjectTypes: 'never', - allowInTypeAliasWithName: false, }, ], - create( - context, - [{ allowInterfaces, allowInTypeAliasWithName, allowObjectTypes }], - ) { + create(context, [{ allowInterfaces, allowWithName, allowObjectTypes }]) { + const allowWithNameTester = allowWithName + ? new RegExp(allowWithName, 'u') + : undefined; + return { ...(allowInterfaces !== 'always' && { TSInterfaceDeclaration(node): void { + if (allowWithNameTester?.test(node.id.name)) { + return; + } + const extend = node.extends; if ( node.body.body.length !== 0 || @@ -154,8 +161,9 @@ export default createRule({ if ( node.members.length || node.parent.type === AST_NODE_TYPES.TSIntersectionType || - (allowInTypeAliasWithName && - node.parent.type === AST_NODE_TYPES.TSTypeAliasDeclaration) + (allowWithNameTester && + node.parent.type === AST_NODE_TYPES.TSTypeAliasDeclaration && + allowWithNameTester.test(node.parent.id.name)) ) { return; } diff --git a/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts b/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts index 414f6f0d5d0b..f8201b32c4fe 100644 --- a/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts +++ b/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts @@ -61,7 +61,19 @@ class Derived {} }, { code: 'type Base = {};', - options: [{ allowInTypeAliasWithName: true }], + options: [{ allowWithName: 'Base' }], + }, + { + code: 'type BaseProps = {};', + options: [{ allowWithName: 'Props$' }], + }, + { + code: 'interface Base {}', + options: [{ allowWithName: 'Base' }], + }, + { + code: 'interface BaseProps {}', + options: [{ allowWithName: 'Props$' }], }, ], invalid: [ @@ -514,7 +526,33 @@ let value: unknown; messageId: 'noEmptyObject', }, ], - options: [{ allowInTypeAliasWithName: true }], + options: [{ allowWithName: 'Base' }], + }, + { + code: 'type Base = {};', + errors: [ + { + column: 13, + line: 1, + endColumn: 15, + endLine: 1, + messageId: 'noEmptyObject', + }, + ], + options: [{ allowWithName: 'Mismatch' }], + }, + { + code: 'interface Base {}', + errors: [ + { + column: 11, + line: 1, + endColumn: 15, + endLine: 1, + messageId: 'noEmptyInterface', + }, + ], + options: [{ allowWithName: '.*Props$' }], }, ], }); diff --git a/packages/typescript-estree/src/parser.ts b/packages/typescript-estree/src/parser.ts index 4c4b7f61a848..59482f03deee 100644 --- a/packages/typescript-estree/src/parser.ts +++ b/packages/typescript-estree/src/parser.ts @@ -81,7 +81,7 @@ function getProgramAndAST( ); } -// eslint-disable-next-line @typescript-eslint/no-empty-interface +// eslint-disable-next-line @typescript-eslint/no-empty-object-type interface EmptyObject {} type AST = TSESTree.Program & (T['comment'] extends true ? { comments: TSESTree.Comment[] } : EmptyObject) & diff --git a/packages/typescript-estree/src/ts-estree/ts-nodes.ts b/packages/typescript-estree/src/ts-estree/ts-nodes.ts index 2a07ee0e7eac..9501c031f6f8 100644 --- a/packages/typescript-estree/src/ts-estree/ts-nodes.ts +++ b/packages/typescript-estree/src/ts-estree/ts-nodes.ts @@ -2,7 +2,7 @@ import type * as ts from 'typescript'; // Workaround to support new TS version features for consumers on old TS versions // Eg: https://github.com/typescript-eslint/typescript-eslint/issues/2388, https://github.com/typescript-eslint/typescript-eslint/issues/2784 -/* eslint-disable @typescript-eslint/no-empty-interface */ +/* eslint-disable @typescript-eslint/no-empty-object-type */ declare module 'typescript' { // added in TS 4.5, deprecated in TS 5.3 export interface AssertClause extends ts.ImportAttributes {} @@ -15,7 +15,7 @@ declare module 'typescript' { export interface ImportAttribute extends ts.Node {} export interface ImportAttributes extends ts.Node {} } -/* eslint-enable @typescript-eslint/no-empty-interface */ +/* eslint-enable @typescript-eslint/no-empty-object-type */ export type TSToken = ts.Token; diff --git a/packages/utils/src/json-schema.ts b/packages/utils/src/json-schema.ts index 73ab2f3ddc89..4f822a322670 100644 --- a/packages/utils/src/json-schema.ts +++ b/packages/utils/src/json-schema.ts @@ -41,7 +41,7 @@ export interface JSONSchema4Object { // Workaround for infinite type recursion // https://github.com/Microsoft/TypeScript/issues/3496#issuecomment-128553540 -// eslint-disable-next-line @typescript-eslint/no-empty-interface +// eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface JSONSchema4Array extends Array {} /** diff --git a/packages/utils/src/ts-eslint/Rule.ts b/packages/utils/src/ts-eslint/Rule.ts index 838a55217a8c..ebbbd8d446e1 100644 --- a/packages/utils/src/ts-eslint/Rule.ts +++ b/packages/utils/src/ts-eslint/Rule.ts @@ -579,7 +579,7 @@ type RuleListenerExitSelectors = { }; type RuleListenerCatchAllBaseCase = Record; // Interface to merge into for anyone that wants to add more selectors -// eslint-disable-next-line @typescript-eslint/no-empty-interface +// eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface RuleListenerExtension { // The code path functions below were introduced in ESLint v8.7.0 but are // intentionally commented out because they cause unresolvable compiler From b7a70b808e17bc5cb691887ab6a348aa4b369242 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Thu, 9 May 2024 13:58:09 -0700 Subject: [PATCH 16/21] snapshot -u --- .../tests/schema-snapshots/no-empty-object-type.shot | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/tests/schema-snapshots/no-empty-object-type.shot b/packages/eslint-plugin/tests/schema-snapshots/no-empty-object-type.shot index 2542efd5441c..632b3ce83ca9 100644 --- a/packages/eslint-plugin/tests/schema-snapshots/no-empty-object-type.shot +++ b/packages/eslint-plugin/tests/schema-snapshots/no-empty-object-type.shot @@ -13,7 +13,10 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos "type": "string" }, "allowObjectTypes": { - "enum": ["always", "never"], + "enum": ["always", "in-type-alias-with-name", "never"], + "type": "string" + }, + "allowWithName": { "type": "string" } }, @@ -27,7 +30,8 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos type Options = [ { allowInterfaces?: 'always' | 'never' | 'with-single-extends'; - allowObjectTypes?: 'always' | 'never'; + allowObjectTypes?: 'always' | 'in-type-alias-with-name' | 'never'; + allowWithName?: string; }, ]; " From 713404a703b7c0bcb1732fe23e0989e76688ea2e Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Thu, 9 May 2024 14:21:21 -0700 Subject: [PATCH 17/21] snapshot -u --- .../docs/rules/no-empty-object-type.mdx | 4 +-- .../no-empty-object-type.shot | 32 +++++++++++++++++-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx b/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx index 37e53aba56e8..3dbb93092f54 100644 --- a/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx +++ b/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx @@ -122,7 +122,7 @@ Examples of code for this rule with `{ allowWithName: 'Props$' }`: -```ts option='{ "allowInterfaces": "Props$" }' showPlaygroundButton +```ts option='{ "allowWithName": "Props$" }' showPlaygroundButton interface InterfaceValue {} type TypeValue = {}; @@ -131,7 +131,7 @@ type TypeValue = {}; -```ts option='{ "allowInterfaces": "Props$" }' showPlaygroundButton +```ts option='{ "allowWithName": "Props$" }' showPlaygroundButton interface InterfaceProps {} type TypeProps = {}; diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-empty-object-type.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-empty-object-type.shot index 4dba11690551..15ee654ff40a 100644 --- a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-empty-object-type.shot +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-empty-object-type.shot @@ -15,12 +15,12 @@ let anyValue: {}; - If you want a type meaning "any value", you probably want \`unknown\` instead. interface AnyObjectA {} - ~~~~~~~~~~ The \`{}\` ("empty object") type allows any non-nullish value, including literals like \`0\` and \`""\`. + ~~~~~~~~~~ An empty interface declaration allows any non-nullish value, including literals like \`0\` and \`""\`. - If that's what you want, disable this lint rule with an inline comment or configure the 'allowInterfaces' rule option. - If you want a type meaning "any object", you probably want \`object\` instead. - If you want a type meaning "any value", you probably want \`unknown\` instead. interface AnyValueA {} - ~~~~~~~~~ The \`{}\` ("empty object") type allows any non-nullish value, including literals like \`0\` and \`""\`. + ~~~~~~~~~ An empty interface declaration allows any non-nullish value, including literals like \`0\` and \`""\`. - If that's what you want, disable this lint rule with an inline comment or configure the 'allowInterfaces' rule option. - If you want a type meaning "any object", you probably want \`object\` instead. - If you want a type meaning "any value", you probably want \`unknown\` instead. @@ -70,3 +70,31 @@ interface Base { interface Derived extends Base {} " `; + +exports[`Validating rule docs no-empty-object-type.mdx code examples ESLint output 4`] = ` +"Incorrect +Options: { "allowWithName": "Props$" } + +interface InterfaceValue {} + ~~~~~~~~~~~~~~ An empty interface declaration allows any non-nullish value, including literals like \`0\` and \`""\`. + - If that's what you want, disable this lint rule with an inline comment or configure the 'allowInterfaces' rule option. + - If you want a type meaning "any object", you probably want \`object\` instead. + - If you want a type meaning "any value", you probably want \`unknown\` instead. + +type TypeValue = {}; + ~~ The \`{}\` ("empty object") type allows any non-nullish value, including literals like \`0\` and \`""\`. + - If that's what you want, disable this lint rule with an inline comment or configure the 'allowObjectTypes' rule option. + - If you want a type meaning "any object", you probably want \`object\` instead. + - If you want a type meaning "any value", you probably want \`unknown\` instead. +" +`; + +exports[`Validating rule docs no-empty-object-type.mdx code examples ESLint output 5`] = ` +"Correct +Options: { "allowWithName": "Props$" } + +interface InterfaceProps {} + +type TypeProps = {}; +" +`; From 218d8e5fc8d63e0dd4653ac2e6aef408c22aa98a Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Thu, 9 May 2024 14:24:25 -0700 Subject: [PATCH 18/21] Fixed up unit tests --- .../src/rules/no-empty-object-type.ts | 1 + .../tests/rules/no-empty-object-type.test.ts | 49 ++++++++++++++++--- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-empty-object-type.ts b/packages/eslint-plugin/src/rules/no-empty-object-type.ts index 47f3eb694c61..ad20103061c6 100644 --- a/packages/eslint-plugin/src/rules/no-empty-object-type.ts +++ b/packages/eslint-plugin/src/rules/no-empty-object-type.ts @@ -113,6 +113,7 @@ export default createRule({ messageId: 'noEmptyInterface', ...(!mergedWithClassDeclaration && { suggest: ['object', 'unknown'].map(replacement => ({ + data: { replacement }, fix(fixer): TSESLint.RuleFix { const id = context.sourceCode.getText(node.id); const typeParam = node.typeParameters diff --git a/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts b/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts index f8201b32c4fe..5ccc5f5c1814 100644 --- a/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts +++ b/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts @@ -81,10 +81,21 @@ class Derived {} code: 'interface Base {}', errors: [ { + column: 11, data: { option: 'allowInterfaces' }, - messageId: 'noEmptyInterface', line: 1, - column: 11, + messageId: 'noEmptyInterface', + suggestions: [ + { + data: { replacement: 'object' }, + messageId: 'replaceEmptyInterface', + output: `type Base = object`, + }, + { + messageId: 'replaceEmptyInterface', + output: `type Base = unknown`, + }, + ], }, ], }, @@ -92,10 +103,21 @@ class Derived {} code: 'interface Base {}', errors: [ { + column: 11, data: { option: 'allowInterfaces' }, - messageId: 'noEmptyInterface', line: 1, - column: 11, + messageId: 'noEmptyInterface', + suggestions: [ + { + data: { replacement: 'object' }, + messageId: 'replaceEmptyInterface', + output: `type Base = object`, + }, + { + messageId: 'replaceEmptyInterface', + output: `type Base = unknown`, + }, + ], }, ], options: [{ allowInterfaces: 'never' }], @@ -112,9 +134,9 @@ class Other {} `, errors: [ { - messageId: 'noEmptyInterfaceWithSuper', - line: 6, column: 11, + line: 6, + messageId: 'noEmptyInterfaceWithSuper', suggestions: [ { messageId: 'replaceEmptyInterfaceWithSuper', @@ -144,9 +166,9 @@ class Derived {} `, errors: [ { - messageId: 'noEmptyInterfaceWithSuper', - line: 6, column: 11, + line: 6, + messageId: 'noEmptyInterfaceWithSuper', }, ], }, @@ -550,6 +572,17 @@ let value: unknown; endColumn: 15, endLine: 1, messageId: 'noEmptyInterface', + suggestions: [ + { + data: { replacement: 'object' }, + messageId: 'replaceEmptyInterface', + output: `type Base = object`, + }, + { + messageId: 'replaceEmptyInterface', + output: `type Base = unknown`, + }, + ], }, ], options: [{ allowWithName: '.*Props$' }], From 9987c2b82f6bc90a2447bb4715a6e5ecec6116b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Sat, 11 May 2024 18:22:46 -0700 Subject: [PATCH 19/21] Update packages/eslint-plugin/docs/rules/no-empty-object-type.mdx Co-authored-by: Kirk Waiblinger --- packages/eslint-plugin/docs/rules/no-empty-object-type.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx b/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx index 3dbb93092f54..9834313cb24e 100644 --- a/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx +++ b/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx @@ -115,7 +115,7 @@ Whether to allow empty object type literals, as one of: ### `allowWithName` A stringified regular expression to allow interfaces and object type aliases with the configured name. -This can be useful if your existing code style still includes a pattern of declaring empty types with `{}` instead of `object`. +This can be useful if your existing code style includes a pattern of declaring empty types with `{}` instead of `object`. Examples of code for this rule with `{ allowWithName: 'Props$' }`: From bd764ef647235a7212f54f683231754431c0001c Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sat, 11 May 2024 18:22:56 -0700 Subject: [PATCH 20/21] docs touchups from Kirk --- .../eslint-plugin/docs/rules/no-empty-object-type.mdx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx b/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx index 9834313cb24e..8bf941a66615 100644 --- a/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx +++ b/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx @@ -33,7 +33,7 @@ If you do have a use case for an API allowing `{}`, you can always configure the Note that this rule does not report on: -- `{}` as a type constituent in an intersection type (e.g. `type NonNullable = T & {}`), as this can be useful in type system operations. +- `{}` as a type constituent in an intersection type (e.g. types like TypeScript's built-in `type NonNullable = T & {}`), as this can be useful in type system operations. - Interfaces that extend from multiple other interfaces. ## Examples @@ -81,12 +81,6 @@ type TypeWith = { property: boolean }; By default, this rule flags both interfaces and object types. -:::warning -We recommend keeping all options to the default and not allowing exceptions to this rule. -The "empty object" type is often confusing even for experienced TypeScript developers. -Consider using `object` or `unknown` as a type instead. -::: - ### `allowInterfaces` Whether to allow empty interfaces, as one of: From 7dc88d75faf8fe3318463d388a24f685f2b60214 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sat, 11 May 2024 21:46:17 -0700 Subject: [PATCH 21/21] replacedBy too --- packages/eslint-plugin/src/rules/no-empty-interface.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/eslint-plugin/src/rules/no-empty-interface.ts b/packages/eslint-plugin/src/rules/no-empty-interface.ts index b3ada33093f3..03d00c1a9535 100644 --- a/packages/eslint-plugin/src/rules/no-empty-interface.ts +++ b/packages/eslint-plugin/src/rules/no-empty-interface.ts @@ -19,6 +19,7 @@ export default createRule({ description: 'Disallow the declaration of empty interfaces', }, deprecated: true, + replacedBy: ['@typescript-eslint/no-empty-object-type'], fixable: 'code', hasSuggestions: true, messages: { pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy