diff --git a/packages/eslint-plugin/docs/rules/no-unnecessary-type-assertion.mdx b/packages/eslint-plugin/docs/rules/no-unnecessary-type-assertion.mdx index 497f843180e3..9ba5fc3d4e85 100644 --- a/packages/eslint-plugin/docs/rules/no-unnecessary-type-assertion.mdx +++ b/packages/eslint-plugin/docs/rules/no-unnecessary-type-assertion.mdx @@ -37,10 +37,6 @@ type Foo = number; const foo = (3 + 5) as Foo; ``` -```ts -const foo = 'foo' as const; -``` - ```ts function foo(x: number): number { return x!; // unnecessary non-null @@ -73,6 +69,18 @@ function foo(x: number | undefined): number { ## Options +### `checkLiteralConstAssertions` + +{/* insert option description */} + +With `@typescript-eslint/no-unnecessary-type-assertion: ["error", { checkLiteralConstAssertions: true }]`, the following is **incorrect** code: + +```ts option='{ "checkLiteralConstAssertions": true }' showPlaygroundButton +const foo = 'foo' as const; +``` + +See [#8721 False positives for "as const" assertions (issue comment)](https://github.com/typescript-eslint/typescript-eslint/issues/8721#issuecomment-2145291966) for more information on this option. + ### `typesToIgnore` {/* insert option description */} diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts index 4e26d6e20aa2..3225e418ce3c 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts @@ -21,6 +21,7 @@ import { export type Options = [ { + checkLiteralConstAssertions?: boolean; typesToIgnore?: string[]; }, ]; @@ -48,6 +49,10 @@ export default createRule({ type: 'object', additionalProperties: false, properties: { + checkLiteralConstAssertions: { + type: 'boolean', + description: 'Whether to check literal const assertions.', + }, typesToIgnore: { type: 'array', description: 'A list of type names to ignore.', @@ -217,6 +222,10 @@ export default createRule({ return false; } + function isTypeLiteral(type: ts.Type) { + return type.isLiteral() || tsutils.isBooleanLiteralType(type); + } + return { 'TSAsExpression, TSTypeAssertion'( node: TSESTree.TSAsExpression | TSESTree.TSTypeAssertion, @@ -230,12 +239,24 @@ export default createRule({ } const castType = services.getTypeAtLocation(node); + const castTypeIsLiteral = isTypeLiteral(castType); + const typeAnnotationIsConstAssertion = isConstAssertion( + node.typeAnnotation, + ); + + if ( + !options.checkLiteralConstAssertions && + castTypeIsLiteral && + typeAnnotationIsConstAssertion + ) { + return; + } + const uncastType = services.getTypeAtLocation(node.expression); const typeIsUnchanged = isTypeUnchanged(uncastType, castType); - - const wouldSameTypeBeInferred = castType.isLiteral() + const wouldSameTypeBeInferred = castTypeIsLiteral ? isImplicitlyNarrowedLiteralDeclaration(node) - : !isConstAssertion(node.typeAnnotation); + : !typeAnnotationIsConstAssertion; if (typeIsUnchanged && wouldSameTypeBeInferred) { context.report({ diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-unnecessary-type-assertion.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-unnecessary-type-assertion.shot index 7dfc8a79864f..5c7cf3a7f5e0 100644 --- a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-unnecessary-type-assertion.shot +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-unnecessary-type-assertion.shot @@ -23,11 +23,6 @@ const foo = (3 + 5) as Foo; Incorrect -const foo = 'foo' as const; - ~~~~~~~~~~~~~~ This assertion is unnecessary since it does not change the type of the expression. - -Incorrect - function foo(x: number): number { return x!; // unnecessary non-null ~~ This assertion is unnecessary since it does not change the type of the expression. @@ -51,6 +46,11 @@ function foo(x: number | undefined): number { return x!; } +Options: { "checkLiteralConstAssertions": true } + +const foo = 'foo' as const; + ~~~~~~~~~~~~~~ This assertion is unnecessary since it does not change the type of the expression. + Options: { "typesToIgnore": ["Foo"] } type Foo = 3; diff --git a/packages/eslint-plugin/tests/rules/no-unnecessary-type-assertion.test.ts b/packages/eslint-plugin/tests/rules/no-unnecessary-type-assertion.test.ts index c28260e79b4a..e8af03e47d2c 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-type-assertion.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-type-assertion.test.ts @@ -429,25 +429,35 @@ declare function foo(bar: T): T; const baz: unknown = {}; foo(baz!); `, - ], - - invalid: [ - // https://github.com/typescript-eslint/typescript-eslint/issues/8737 { code: 'const a = `a` as const;', - errors: [{ line: 1, messageId: 'unnecessaryAssertion' }], - output: 'const a = `a`;', }, { code: "const a = 'a' as const;", - errors: [{ line: 1, messageId: 'unnecessaryAssertion' }], - output: "const a = 'a';", }, { code: "const a = 'a';", - errors: [{ line: 1, messageId: 'unnecessaryAssertion' }], - output: "const a = 'a';", }, + { + code: ` +class T { + readonly a = 'a' as const; +} + `, + }, + { + code: ` +enum T { + Value1, + Value2, +} +declare const a: T.Value1; +const b = a as const; + `, + }, + ], + + invalid: [ { code: 'const foo = <3>3;', errors: [{ column: 13, line: 1, messageId: 'unnecessaryAssertion' }], @@ -1209,24 +1219,6 @@ var x = 1; }, { code: ` -class T { - readonly a = 'a' as const; -} - `, - errors: [ - { - line: 3, - messageId: 'unnecessaryAssertion', - }, - ], - output: ` -class T { - readonly a = 'a'; -} - `, - }, - { - code: ` class T { readonly a = 3 as 3; } @@ -1319,31 +1311,6 @@ enum T { Value2, } -declare const a: T.Value1; -const b = a; - `, - }, - { - code: ` -enum T { - Value1, - Value2, -} - -declare const a: T.Value1; -const b = a as const; - `, - errors: [ - { - messageId: 'unnecessaryAssertion', - }, - ], - output: ` -enum T { - Value1, - Value2, -} - declare const a: T.Value1; const b = a; `, @@ -1380,5 +1347,105 @@ const baz: unknown = {}; foo(baz); `, }, + { + code: 'const a = true as const;', + errors: [{ line: 1, messageId: 'unnecessaryAssertion' }], + options: [{ checkLiteralConstAssertions: true }], + output: 'const a = true;', + }, + { + code: 'const a = true;', + errors: [{ line: 1, messageId: 'unnecessaryAssertion' }], + options: [{ checkLiteralConstAssertions: true }], + output: 'const a = true;', + }, + { + code: 'const a = 1 as const;', + errors: [{ line: 1, messageId: 'unnecessaryAssertion' }], + options: [{ checkLiteralConstAssertions: true }], + output: 'const a = 1;', + }, + { + code: 'const a = 1;', + errors: [{ line: 1, messageId: 'unnecessaryAssertion' }], + options: [{ checkLiteralConstAssertions: true }], + output: 'const a = 1;', + }, + { + code: 'const a = 1n as const;', + errors: [{ line: 1, messageId: 'unnecessaryAssertion' }], + options: [{ checkLiteralConstAssertions: true }], + output: 'const a = 1n;', + }, + { + code: 'const a = 1n;', + errors: [{ line: 1, messageId: 'unnecessaryAssertion' }], + options: [{ checkLiteralConstAssertions: true }], + output: 'const a = 1n;', + }, + // https://github.com/typescript-eslint/typescript-eslint/issues/8737 + { + code: 'const a = `a` as const;', + errors: [{ line: 1, messageId: 'unnecessaryAssertion' }], + options: [{ checkLiteralConstAssertions: true }], + output: 'const a = `a`;', + }, + { + code: "const a = 'a' as const;", + errors: [{ line: 1, messageId: 'unnecessaryAssertion' }], + options: [{ checkLiteralConstAssertions: true }], + output: "const a = 'a';", + }, + { + code: "const a = 'a';", + errors: [{ line: 1, messageId: 'unnecessaryAssertion' }], + options: [{ checkLiteralConstAssertions: true }], + output: "const a = 'a';", + }, + { + code: ` +class T { + readonly a = 'a' as const; +} + `, + errors: [ + { + line: 3, + messageId: 'unnecessaryAssertion', + }, + ], + options: [{ checkLiteralConstAssertions: true }], + output: ` +class T { + readonly a = 'a'; +} + `, + }, + { + code: ` +enum T { + Value1, + Value2, +} + +declare const a: T.Value1; +const b = a as const; + `, + errors: [ + { + messageId: 'unnecessaryAssertion', + }, + ], + options: [{ checkLiteralConstAssertions: true }], + output: ` +enum T { + Value1, + Value2, +} + +declare const a: T.Value1; +const b = a; + `, + }, ], }); diff --git a/packages/eslint-plugin/tests/schema-snapshots/no-unnecessary-type-assertion.shot b/packages/eslint-plugin/tests/schema-snapshots/no-unnecessary-type-assertion.shot index bf9e0527ae55..8a0ba9015278 100644 --- a/packages/eslint-plugin/tests/schema-snapshots/no-unnecessary-type-assertion.shot +++ b/packages/eslint-plugin/tests/schema-snapshots/no-unnecessary-type-assertion.shot @@ -5,6 +5,10 @@ { "additionalProperties": false, "properties": { + "checkLiteralConstAssertions": { + "description": "Whether to check literal const assertions.", + "type": "boolean" + }, "typesToIgnore": { "description": "A list of type names to ignore.", "items": { @@ -22,6 +26,8 @@ type Options = [ { + /** Whether to check literal const assertions. */ + checkLiteralConstAssertions?: boolean; /** A list of type names to ignore. */ typesToIgnore?: string[]; },