Skip to content

Commit 4c91ed5

Browse files
fix(eslint-plugin): [no-unsafe-type-assertion] fix for unsafe assertion to a constrained type parameter (typescript-eslint#10461)
* Add failing test case for unsafe type assertion with constrained generic types * Use actual type instead of “constrained type” for no-unsafe-type-assertion * tweak test cases * Custom error message for “assignable to the constraint” case * rename “assignable to the constraint” error message * spelling * tweak message * Add new test cases from @kirkwaiblinger * Move isAssignableToConstraint check into isAssertionSafe * Properly handle unconstrained type parameters, including new error message + light refactor to prefer early returns over nested conditionals * distinguish between “parameter extends other parameter” and “parameter extends unconstrained parameter” * clean up extra pieces in unit tests unit tests should only test one thing * test lines --------- Co-authored-by: Kirk Waiblinger <kirk.waiblinger@gmail.com>
1 parent 91e7217 commit 4c91ed5

File tree

2 files changed

+386
-19
lines changed

2 files changed

+386
-19
lines changed

packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import * as ts from 'typescript';
55

66
import {
77
createRule,
8-
getConstrainedTypeAtLocation,
98
getParserServices,
109
isTypeAnyType,
1110
isTypeUnknownType,
@@ -25,8 +24,12 @@ export default createRule({
2524
'Unsafe assertion from {{type}} detected: consider using type guards or a safer assertion.',
2625
unsafeToAnyTypeAssertion:
2726
'Unsafe assertion to {{type}} detected: consider using a more specific type to ensure safety.',
27+
unsafeToUnconstrainedTypeAssertion:
28+
"Unsafe type assertion: '{{type}}' could be instantiated with an arbitrary type which could be unrelated to the original type.",
2829
unsafeTypeAssertion:
2930
"Unsafe type assertion: type '{{type}}' is more narrow than the original type.",
31+
unsafeTypeAssertionAssignableToConstraint:
32+
"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.",
3033
},
3134
schema: [],
3235
},
@@ -49,14 +52,8 @@ export default createRule({
4952
function checkExpression(
5053
node: TSESTree.TSAsExpression | TSESTree.TSTypeAssertion,
5154
): void {
52-
const expressionType = getConstrainedTypeAtLocation(
53-
services,
54-
node.expression,
55-
);
56-
const assertedType = getConstrainedTypeAtLocation(
57-
services,
58-
node.typeAnnotation,
59-
);
55+
const expressionType = services.getTypeAtLocation(node.expression);
56+
const assertedType = services.getTypeAtLocation(node.typeAnnotation);
6057

6158
if (expressionType === assertedType) {
6259
return;
@@ -115,24 +112,60 @@ export default createRule({
115112

116113
// Use the widened type in case of an object literal so `isTypeAssignableTo()`
117114
// won't fail on excess property check.
118-
const nodeWidenedType = isObjectLiteralType(expressionType)
115+
const expressionWidenedType = isObjectLiteralType(expressionType)
119116
? checker.getWidenedType(expressionType)
120117
: expressionType;
121118

122119
const isAssertionSafe = checker.isTypeAssignableTo(
123-
nodeWidenedType,
120+
expressionWidenedType,
124121
assertedType,
125122
);
123+
if (isAssertionSafe) {
124+
return;
125+
}
126126

127-
if (!isAssertionSafe) {
128-
context.report({
129-
node,
130-
messageId: 'unsafeTypeAssertion',
131-
data: {
132-
type: checker.typeToString(assertedType),
133-
},
134-
});
127+
// Produce a more specific error message when targeting a type parameter
128+
if (tsutils.isTypeParameter(assertedType)) {
129+
const assertedTypeConstraint =
130+
checker.getBaseConstraintOfType(assertedType);
131+
if (!assertedTypeConstraint) {
132+
// asserting to an unconstrained type parameter is unsafe
133+
context.report({
134+
node,
135+
messageId: 'unsafeToUnconstrainedTypeAssertion',
136+
data: {
137+
type: checker.typeToString(assertedType),
138+
},
139+
});
140+
return;
141+
}
142+
143+
// special case message if the original type is assignable to the
144+
// constraint of the target type parameter
145+
const isAssignableToConstraint = checker.isTypeAssignableTo(
146+
expressionWidenedType,
147+
assertedTypeConstraint,
148+
);
149+
if (isAssignableToConstraint) {
150+
context.report({
151+
node,
152+
messageId: 'unsafeTypeAssertionAssignableToConstraint',
153+
data: {
154+
type: checker.typeToString(assertedType),
155+
},
156+
});
157+
return;
158+
}
135159
}
160+
161+
// General error message
162+
context.report({
163+
node,
164+
messageId: 'unsafeTypeAssertion',
165+
data: {
166+
type: checker.typeToString(assertedType),
167+
},
168+
});
136169
}
137170

138171
return {

0 commit comments

Comments
 (0)