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 c73d8717c3e1..5dfed1cff579 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts @@ -2,7 +2,11 @@ import type { Scope } from '@typescript-eslint/scope-manager'; import type { TSESTree } from '@typescript-eslint/utils'; import type { ReportFixFunction } from '@typescript-eslint/utils/ts-eslint'; -import { AST_NODE_TYPES, AST_TOKEN_TYPES } from '@typescript-eslint/utils'; +import { + AST_NODE_TYPES, + AST_TOKEN_TYPES, + ASTUtils, +} from '@typescript-eslint/utils'; import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; @@ -208,6 +212,63 @@ export default createRule({ return false; } + function doesExpressionHaveFreshLiterals( + node: TSESTree.Expression, + ): boolean { + // Actual literals don't seem to have the type of a "fresh literal" + if ( + node.type === AST_NODE_TYPES.TemplateLiteral || + node.type === AST_NODE_TYPES.Literal + ) { + return true; + } + + if (node.type === AST_NODE_TYPES.Identifier) { + // It seems "fresh literals" have a slightly different type at their + // definition rather than their reference. + const scope = context.sourceCode.getScope(node); + const superVar = ASTUtils.findVariable(scope, node.name); + + if (superVar == null) { + return true; + } + + const definition = superVar.defs.at(0); + + if (definition == null) { + return true; + } + + const typeAtDefinition = services.getTypeAtLocation(definition.node); + + if (tsutils.isUnionType(typeAtDefinition)) { + return tsutils + .unionTypeParts(typeAtDefinition) + .some(part => isFreshLiteralType(part)); + } + + return isFreshLiteralType(typeAtDefinition); + } + + if (node.type === AST_NODE_TYPES.ConditionalExpression) { + return ( + doesExpressionHaveFreshLiterals(node.alternate) || + doesExpressionHaveFreshLiterals(node.consequent) + ); + } + + if ( + tsutils.isTypeFlagSet( + services.getTypeAtLocation(node), + ts.TypeFlags.EnumLiteral | ts.TypeFlags.UniqueESSymbol, + ) + ) { + return true; + } + + return false; + } + return { 'TSAsExpression, TSTypeAssertion'( node: TSESTree.TSAsExpression | TSESTree.TSTypeAssertion, @@ -224,7 +285,11 @@ export default createRule({ const uncastType = services.getTypeAtLocation(node.expression); const typeIsUnchanged = isTypeUnchanged(uncastType, castType); - const wouldSameTypeBeInferred = castType.isLiteral() + const hasFreshLiterals = doesExpressionHaveFreshLiterals( + node.expression, + ); + + const wouldSameTypeBeInferred = hasFreshLiterals ? isImplicitlyNarrowedConstDeclaration(node) : !isConstAssertion(node.typeAnnotation); @@ -387,3 +452,8 @@ export default createRule({ }; }, }); + +// https://github.com/microsoft/TypeScript/blob/21c1a61b49082915f93e3327dad0d73205cf4273/src/compiler/checker.ts/#L19679-L19681 +function isFreshLiteralType(type: ts.Type) { + return tsutils.isFreshableType(type) && type.freshType === type; +} 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..f7db71d87853 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 @@ -358,6 +358,117 @@ if (Math.random()) { x!; `, }, + "let a = (Date.now() % 2 ? 'a' : 'b') as 'a' | 'b';", + ` + const state: 'expired' | 'pending' = 'pending'; + + function main() { + return { + type: \`\${state}Request\` as \`\${typeof state}Request\`, + }; + } + `, + ` +let conditionFreshCasted = (Math.random() > 0.5 ? 'foo' : 'bar') as + | 'foo' + | 'bar'; + `, + ` +let a = (Math.random() > 0.5 ? 'foo' : 'bar') as 'foo' | 'bar'; + `, + ` +let a = (Math.random() > 0.5 ? 1 : 2) as 1 | 2; + `, + ` +declare const foo: 'foo'; +const bar = 'bar'; + +let a = (Math.random() > 0.5 ? foo : bar) as bar | foo; + `, + ` +const foo = 'foo'; +let a = foo as 'foo'; + `, + ` +const foo = 'foo'; +function f() { + return foo as 'foo'; +} + `, + ` +const foo = 'foo'; +const a = { + foo: foo as 'foo', +}; + `, + ` +function f() { + return 'foo'; +} + +let callCasted = f() as 'foo'; + `, + ` +const obj = { + foo: 'foo', +}; + +let s = obj.foo as 'foo'; + `, + ` +const a = 1; + +foo(a as 1); + +function foo(a: T) {} + `, + 'let a = b as 1;', + ` +const a = Math.random() > 0.5 ? 'foo' : 'bar'; + +let c = a as 'bar' | 'foo'; + `, + ` +enum Foo { + Bar, + Bazz, +} + +const data = { + x: Foo.Bar as Foo.Bar, +}; + `, + ` +enum Foo { + Bar, + Bazz, +} + +const a = Foo.Bar; + +const data = { + x: a as Foo.Bar, +}; + `, + ` +enum Foo { + Bar, + Bazz, +} + +const a = Foo; + +const data = { + x: a.Bar as Foo.Bar, +}; + `, + ` +class Foo { + hey?: string; +} + +let b = (Math.random() > 0.5 ? new Foo() : '1') as '1' | Foo; + `, ], invalid: [ @@ -1151,5 +1262,271 @@ const a = ''; const b: string | undefined = (a ? undefined : a); `, }, + { + code: "const a = (Date.now() % 2 ? 'a' : 'b') as 'a' | 'b';", + errors: [ + { + column: 11, + line: 1, + messageId: 'unnecessaryAssertion', + }, + ], + output: `const a = (Date.now() % 2 ? 'a' : 'b');`, + }, + { + code: ` +declare const foo: 'foo'; +declare const bar: 'bar'; + +let a = (Math.random() > 0.5 ? foo : bar) as 'bar' | 'foo'; + `, + errors: [ + { + column: 9, + line: 5, + messageId: 'unnecessaryAssertion', + }, + ], + output: ` +declare const foo: 'foo'; +declare const bar: 'bar'; + +let a = (Math.random() > 0.5 ? foo : bar); + `, + }, + { + code: ` +declare const foo: 'foo'; +let a = foo as 'foo'; + `, + errors: [ + { + column: 9, + line: 3, + messageId: 'unnecessaryAssertion', + }, + ], + output: ` +declare const foo: 'foo'; +let a = foo; + `, + }, + { + code: ` +declare const foo: 'foo'; +function f() { + return foo as 'foo'; +} + `, + errors: [ + { + column: 10, + line: 4, + messageId: 'unnecessaryAssertion', + }, + ], + output: ` +declare const foo: 'foo'; +function f() { + return foo; +} + `, + }, + { + code: ` +declare const foo: 'foo'; +const a = { + foo: foo as 'foo', +}; + `, + errors: [ + { + column: 8, + line: 4, + messageId: 'unnecessaryAssertion', + }, + ], + output: ` +declare const foo: 'foo'; +const a = { + foo: foo, +}; + `, + }, + { + code: ` +function f(): 'foo' { + return 'foo'; +} + +let callCasted = f() as 'foo'; + `, + errors: [ + { + column: 18, + line: 6, + messageId: 'unnecessaryAssertion', + }, + ], + output: ` +function f(): 'foo' { + return 'foo'; +} + +let callCasted = f(); + `, + }, + { + code: ` +const obj = { + foo: 'foo' as const, +}; + +let s = obj.foo as 'foo'; + `, + errors: [ + { + column: 9, + line: 6, + messageId: 'unnecessaryAssertion', + }, + ], + output: ` +const obj = { + foo: 'foo' as const, +}; + +let s = obj.foo; + `, + }, + { + code: ` +declare const a: 1; + +foo(a as 1); + +function foo(a: T) {} + `, + errors: [ + { + column: 5, + line: 4, + messageId: 'unnecessaryAssertion', + }, + ], + output: ` +declare const a: 1; + +foo(a); + +function foo(a: T) {} + `, + }, + { + code: ` +const state: 'expired' | 'pending' = 'pending'; + +function main() { + return { + type: state as 'expired' | 'pending', + }; +} + `, + errors: [ + { + column: 11, + line: 6, + messageId: 'unnecessaryAssertion', + }, + ], + output: ` +const state: 'expired' | 'pending' = 'pending'; + +function main() { + return { + type: state, + }; +} + `, + }, + { + code: ` +const state: 'expired' | 'pending' = 'pending'; + +class Example { + type = state as 'expired' | 'pending'; +} + `, + errors: [ + { + column: 10, + line: 5, + messageId: 'unnecessaryAssertion', + }, + ], + output: ` +const state: 'expired' | 'pending' = 'pending'; + +class Example { + type = state; +} + `, + }, + { + code: ` +enum Foo { + Bar, + Bazz, +} + +declare const a: Foo.Bar; + +const data = { + x: a as Foo.Bar, +}; + `, + errors: [ + { + column: 6, + line: 10, + messageId: 'unnecessaryAssertion', + }, + ], + output: ` +enum Foo { + Bar, + Bazz, +} + +declare const a: Foo.Bar; + +const data = { + x: a, +}; + `, + }, + { + code: ` +class Foo { + hey?: string; +} + +const b = (Math.random() > 0.5 ? new Foo() : '1') as '1' | Foo; + `, + errors: [ + { + column: 11, + line: 6, + messageId: 'unnecessaryAssertion', + }, + ], + output: ` +class Foo { + hey?: string; +} + +const b = (Math.random() > 0.5 ? new Foo() : '1'); + `, + }, ], });