diff --git a/packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts b/packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts index 649dfd12ae27..988a140d60df 100644 --- a/packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts +++ b/packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts @@ -5,7 +5,6 @@ import * as ts from 'typescript'; import { createRule, - getConstrainedTypeAtLocation, getParserServices, isTypeAnyType, isTypeUnknownType, @@ -25,8 +24,12 @@ export default createRule({ 'Unsafe assertion from {{type}} detected: consider using type guards or a safer assertion.', unsafeToAnyTypeAssertion: 'Unsafe assertion to {{type}} detected: consider using a more specific type to ensure safety.', + unsafeToUnconstrainedTypeAssertion: + "Unsafe type assertion: '{{type}}' could be instantiated with an arbitrary type which could be unrelated to the original type.", unsafeTypeAssertion: "Unsafe type assertion: type '{{type}}' is more narrow than the original type.", + unsafeTypeAssertionAssignableToConstraint: + "Unsafe type assertion: the original type is assignable to the constraint of type '{{type}}', but '{{type}}' could be instantiated with a different subtype of its constraint.", }, schema: [], }, @@ -49,14 +52,8 @@ export default createRule({ function checkExpression( node: TSESTree.TSAsExpression | TSESTree.TSTypeAssertion, ): void { - const expressionType = getConstrainedTypeAtLocation( - services, - node.expression, - ); - const assertedType = getConstrainedTypeAtLocation( - services, - node.typeAnnotation, - ); + const expressionType = services.getTypeAtLocation(node.expression); + const assertedType = services.getTypeAtLocation(node.typeAnnotation); if (expressionType === assertedType) { return; @@ -115,24 +112,60 @@ export default createRule({ // Use the widened type in case of an object literal so `isTypeAssignableTo()` // won't fail on excess property check. - const nodeWidenedType = isObjectLiteralType(expressionType) + const expressionWidenedType = isObjectLiteralType(expressionType) ? checker.getWidenedType(expressionType) : expressionType; const isAssertionSafe = checker.isTypeAssignableTo( - nodeWidenedType, + expressionWidenedType, assertedType, ); + if (isAssertionSafe) { + return; + } - if (!isAssertionSafe) { - context.report({ - node, - messageId: 'unsafeTypeAssertion', - data: { - type: checker.typeToString(assertedType), - }, - }); + // Produce a more specific error message when targeting a type parameter + if (tsutils.isTypeParameter(assertedType)) { + const assertedTypeConstraint = + checker.getBaseConstraintOfType(assertedType); + if (!assertedTypeConstraint) { + // asserting to an unconstrained type parameter is unsafe + context.report({ + node, + messageId: 'unsafeToUnconstrainedTypeAssertion', + data: { + type: checker.typeToString(assertedType), + }, + }); + return; + } + + // special case message if the original type is assignable to the + // constraint of the target type parameter + const isAssignableToConstraint = checker.isTypeAssignableTo( + expressionWidenedType, + assertedTypeConstraint, + ); + if (isAssignableToConstraint) { + context.report({ + node, + messageId: 'unsafeTypeAssertionAssignableToConstraint', + data: { + type: checker.typeToString(assertedType), + }, + }); + return; + } } + + // General error message + context.report({ + node, + messageId: 'unsafeTypeAssertion', + data: { + type: checker.typeToString(assertedType), + }, + }); } return { diff --git a/packages/eslint-plugin/tests/rules/no-unsafe-type-assertion.test.ts b/packages/eslint-plugin/tests/rules/no-unsafe-type-assertion.test.ts index aa73bc2dae4d..0c40f2996676 100644 --- a/packages/eslint-plugin/tests/rules/no-unsafe-type-assertion.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unsafe-type-assertion.test.ts @@ -1041,3 +1041,337 @@ a as Bar; ], }); }); + +describe('generic assertions', () => { + ruleTester.run('no-unsafe-type-assertion', rule, { + valid: [ + ` +type Obj = { foo: string }; +function func(a: T) { + const b = a as T; +} + `, + ` +function parameterExtendsOtherParameter( + x: T, + y: V, +) { + y as T; +} + `, + ` +function parameterExtendsUnconstrainedParameter(x: T, y: V) { + y as T; +} + `, + ` +function unconstrainedToUnknown(x: T) { + x as unknown; +} + `, + ` +function stringToWider(x: T) { + x as number | string; // allowed +} + `, + ], + invalid: [ + { + code: ` +type Obj = { foo: string }; +function func() { + const myObj = { foo: 'hi' } as T; +} + `, + errors: [ + { + column: 17, + endColumn: 35, + endLine: 4, + line: 4, + messageId: 'unsafeTypeAssertionAssignableToConstraint', + }, + ], + }, + { + code: ` +type Obj = { foo: string }; +function func() { + const o: Obj = { foo: 'hi' }; + const myObj = o as T; +} + `, + errors: [ + { + column: 17, + endColumn: 23, + endLine: 5, + line: 5, + messageId: 'unsafeTypeAssertionAssignableToConstraint', + }, + ], + }, + // https://github.com/typescript-eslint/typescript-eslint/issues/10453#issuecomment-2520964068 + // the custom error message should only occur if the expression type is + // *actually* assignable to the constraint of the asserted type + { + code: ` +export function myfunc( + input: number, +): CustomObjectT { + const newCustomObject = input as CustomObjectT; + return newCustomObject; +} + `, + errors: [ + { + column: 27, + endColumn: 49, + endLine: 5, + line: 5, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + // https://github.com/typescript-eslint/typescript-eslint/pull/10461#discussion_r1873887553 + // 1. non-parameter -> parameter assertions + { + code: ` +function unknownConstraint(x: T, y: string) { + y as T; // banned; generic arbitrary subtype +} + `, + errors: [ + { + column: 3, + endColumn: 9, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertionAssignableToConstraint', + }, + ], + }, + { + code: ` +function unconstrained(x: T, y: string) { + y as T; +} + `, + errors: [ + { + column: 3, + endColumn: 9, + endLine: 3, + line: 3, + messageId: 'unsafeToUnconstrainedTypeAssertion', + }, + ], + }, + { + code: ` +// constraint of any functions like constraint of \`unknown\` +// (even the TS error message has this verbiage) +function anyConstraint(x: T, y: string) { + y as T; // banned; generic arbitrary subtype +} + `, + errors: [ + { + column: 3, + endColumn: 9, + endLine: 5, + line: 5, + messageId: 'unsafeTypeAssertionAssignableToConstraint', + }, + ], + }, + { + code: ` +function constraintWiderThanUncastType( + x: T, + y: string, +) { + y as T; // banned; assignable to constraint +} + `, + errors: [ + { + column: 3, + endColumn: 9, + endLine: 6, + line: 6, + messageId: 'unsafeTypeAssertionAssignableToConstraint', + }, + ], + }, + { + code: ` +function constraintEqualUncastType(x: T, y: string) { + y as T; // banned; assignable to constraint +} + `, + errors: [ + { + column: 3, + endColumn: 9, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertionAssignableToConstraint', + }, + ], + }, + { + code: ` +function constraintNarrowerThanUncastType( + x: T, + y: string | number, +) { + y as T; // banned; *not* assignable to constraint +} + `, + errors: [ + { + column: 3, + endColumn: 9, + endLine: 6, + line: 6, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + { + code: ` +function assertFromAny(x: T, y: any) { + y as T; // banned; just an \`any\` complaint. Not a generic subtype. +} + `, + errors: [ + { + column: 3, + endColumn: 9, + endLine: 3, + line: 3, + messageId: 'unsafeOfAnyTypeAssertion', + }, + ], + }, + // 2. parameter -> parameter assertions + { + code: ` +function parameterExtendsOtherParameter( + x: T, + y: V, +) { + x as V; // banned; assignable to constraint +} + `, + errors: [ + { + column: 3, + endColumn: 9, + endLine: 6, + line: 6, + messageId: 'unsafeTypeAssertionAssignableToConstraint', + }, + ], + }, + { + code: ` +function parameterExtendsUnconstrainedParameter(x: T, y: V) { + x as V; // banned; unconstrained arbitrary type +} + `, + errors: [ + { + column: 3, + endColumn: 9, + endLine: 3, + line: 3, + messageId: 'unsafeToUnconstrainedTypeAssertion', + }, + ], + }, + { + code: ` +function twoUnconstrained(x: T, y: V) { + y as T; +} + `, + errors: [ + { + column: 3, + endColumn: 9, + endLine: 3, + line: 3, + messageId: 'unsafeToUnconstrainedTypeAssertion', + }, + ], + }, + // 2. parameter -> non-parameter assertions + { + code: ` +function toNarrower(x: T, y: string) { + x as string; // banned; ordinary 'string' narrower than 'T'. +} + `, + errors: [ + { + column: 3, + endColumn: 14, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + { + code: ` +function unconstrainedToAny(x: T) { + x as any; +} + `, + errors: [ + { + column: 3, + endColumn: 11, + endLine: 3, + line: 3, + messageId: 'unsafeToAnyTypeAssertion', + }, + ], + }, + { + code: ` +function stringToAny(x: T) { + x as any; +} + `, + errors: [ + { + column: 3, + endColumn: 11, + endLine: 3, + line: 3, + messageId: 'unsafeToAnyTypeAssertion', + }, + ], + }, + { + code: ` +function stringToNarrower(x: T) { + x as 'a' | 'b'; +} + `, + errors: [ + { + column: 3, + endColumn: 17, + endLine: 3, + line: 3, + messageId: 'unsafeTypeAssertion', + }, + ], + }, + ], + }); +});