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 ba8b8be80b60..83c8afe9b78c 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts @@ -156,23 +156,32 @@ export default createRule({ ); } - function isImplicitlyNarrowedConstDeclaration({ + function isTemplateLiteralWithExpressions(expression: TSESTree.Expression) { + return ( + expression.type === AST_NODE_TYPES.TemplateLiteral && + expression.expressions.length !== 0 + ); + } + + function isImplicitlyNarrowedLiteralDeclaration({ expression, parent, }: TSESTree.TSAsExpression | TSESTree.TSTypeAssertion): boolean { + /** + * Even on `const` variable declarations, template literals with expressions can sometimes be widened without a type assertion. + * @see https://github.com/typescript-eslint/typescript-eslint/issues/8737 + */ + if (isTemplateLiteralWithExpressions(expression)) { + return false; + } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const maybeDeclarationNode = parent.parent!; - const isTemplateLiteralWithExpressions = - expression.type === AST_NODE_TYPES.TemplateLiteral && - expression.expressions.length !== 0; + return ( - maybeDeclarationNode.type === AST_NODE_TYPES.VariableDeclaration && - maybeDeclarationNode.kind === 'const' && - /** - * Even on `const` variable declarations, template literals with expressions can sometimes be widened without a type assertion. - * @see https://github.com/typescript-eslint/typescript-eslint/issues/8737 - */ - !isTemplateLiteralWithExpressions + (maybeDeclarationNode.type === AST_NODE_TYPES.VariableDeclaration && + maybeDeclarationNode.kind === 'const') || + (parent.type === AST_NODE_TYPES.PropertyDefinition && parent.readonly) ); } @@ -225,7 +234,7 @@ export default createRule({ const typeIsUnchanged = isTypeUnchanged(uncastType, castType); const wouldSameTypeBeInferred = castType.isLiteral() - ? isImplicitlyNarrowedConstDeclaration(node) + ? isImplicitlyNarrowedLiteralDeclaration(node) : !isConstAssertion(node.typeAnnotation); if (typeIsUnchanged && wouldSameTypeBeInferred) { 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 db8309164efb..a8a1e132e8e2 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 @@ -151,6 +151,28 @@ const y: number = x!; const x: number | null = null; class Foo { prop: number = x!; +} + `, + ` +class T { + a = 'a' as const; +} + `, + ` +class T { + a = 3 as 3; +} + `, + ` +const foo = 'foo'; + +class T { + readonly test = \`\${foo}\` as const; +} + `, + ` +class T { + readonly a = { foo: 'foo' } as const; } `, ` @@ -1138,6 +1160,82 @@ 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; +} + `, + errors: [ + { + line: 3, + messageId: 'unnecessaryAssertion', + }, + ], + output: ` +class T { + readonly a = 3; +} + `, + }, + { + code: ` +type S = 10; + +class T { + readonly a = 10 as S; +} + `, + errors: [ + { + line: 5, + messageId: 'unnecessaryAssertion', + }, + ], + output: ` +type S = 10; + +class T { + readonly a = 10; +} + `, + }, + { + code: ` +class T { + readonly a = (3 + 5) as number; +} + `, + errors: [ + { + line: 3, + messageId: 'unnecessaryAssertion', + }, + ], + output: ` +class T { + readonly a = (3 + 5); +} + `, + }, + { + code: ` const a = ''; const b: string | undefined = (a ? undefined : a)!; `,