8000 feat(eslint-plugin): new rule `no-unsafe-type-assertion` (#10051) · omril1/typescript-eslint@1feb829 · GitHub
[go: up one dir, main page]

Skip to content

Commit 1feb829

Browse files
ronamikirkwaiblingerJoshuaKGoldberg
authored andcommitted
feat(eslint-plugin): new rule no-unsafe-type-assertion (typescript-eslint#10051)
* initial implementation * tests * docs * more tests * use checker.typeToString() over getTypeName() * use link * oops * add tests * remove unnecessary typescript 5.4 warning * adjust format to new rules * update error message to be more concise * match implementation to be inline with no-unsafe-* rules * rework tests * refactor * update snapshots * fix error message showing original type instead of asserted type * update snapshots * add a warning for object stubbing on test files * fix linting * adjust test to lint fixes * simplify type comparison * rework code-comments and rename variables * rework the opening paragraph to make it more beginner-friendly * Update packages/eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx Co-authored-by: Kirk Waiblinger <kirk.waiblinger@gmail.com> * fix: narrow/widen in description --------- Co-authored-by: Kirk Waiblinger <kirk.waiblinger@gmail.com> Co-authored-by: Josh Goldberg <git@joshuakgoldberg.com>
1 parent ea3e06d commit 1feb829

File tree

10 files changed

+1305
-0
lines changed

10 files changed

+1305
-0
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
---
2+
description: 'Disallow type assertions that narrow a type.'
3+
---
4+
5+
import Tabs from '@theme/Tabs';
6+
import TabItem from '@theme/TabItem';
7+
8+
> 🛑 This file is source code, not the primary documentation location! 🛑
9+
>
10+
> See **https://typescript-eslint.io/rules/no-unsafe-type-assertion** for documentation.
11+
12+
Type assertions are a way to tell TypeScript what the type of a value is. This can be useful but also unsafe if you use type assertions to narrow down a type.
13+
14+
This rule forbids using type assertions to narrow a type, as this bypasses TypeScript's type-checking. Type assertions that broaden a type are safe because TypeScript essentially knows _less_ about a type.
15+
16+
Instead of using type assertions to narrow a type, it's better to rely on type guards, which help avoid potential runtime errors caused by unsafe type assertions.
17+
18+
## Examples
19+
20+
<Tabs>
21+
<TabItem value="❌ Incorrect">
22+
23+
```ts
24+
function f() {
25+
return Math.random() < 0.5 ? 42 : 'oops';
26+
}
27+
28+
const z = f() as number;
29+
30+
const items = [1, '2', 3, '4'];
31+
32+
const number = items[0] as number;
33+
```
34+
35+
</TabItem>
36+
<TabItem value="✅ Correct">
37+
38+
```ts
39+
function f() {
40+
return Math.random() < 0.5 ? 42 : 'oops';
41+
}
42+
43+
const z = f() as number | string | boolean;
44+
45+
const items = [1, '2', 3, '4'];
46+
47+
const number = items[0] as number | string | undefined;
48+
```
49+
50+
</TabItem>
51+
</Tabs>
52+
53+
## When Not To Use It
54+
55+
If your codebase has many unsafe type assertions, then it may be difficult to enable this rule.
56+
It may be easier to skip the `no-unsafe-*` rules pending increasing type safety in unsafe areas of your project.
57+
You might consider using [ESLint disable comments](https://eslint.org/docs/latest/use/configure/rules#using-configuration-comments-1) for those specific situations instead of completely disabling this rule.
58+
59+
If your project frequently stubs objects in test files, the rule may trigger a lot of reports. Consider disabling the rule for such files to reduce frequent warnings.
60+
61+
## Further Reading
62+
63+
- More on TypeScript's [type assertions](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-assertions)

packages/eslint-plugin/src/configs/all.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ export = {
106106
'@typescript-eslint/no-unsafe-function-type': 'error',
107107
'@typescript-eslint/no-unsafe-member-access': 'error',
108108
'@typescript-eslint/no-unsafe-return': 'error',
109+
'@typescript-eslint/no-unsafe-type-assertion': 'error',
109110
'@typescript-eslint/no-unsafe-unary-minus': 'error',
110111
'no-unused-expressions': 'off',
111112
'@typescript-eslint/no-unused-expressions': 'error',

packages/eslint-plugin/src/configs/disable-type-checked.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export = {
4040
'@typescript-eslint/no-unsafe-enum-comparison': 'off',
4141
'@typescript-eslint/no-unsafe-member-access': 'off',
4242
'@typescript-eslint/no-unsafe-return': 'off',
43+
'@typescript-eslint/no-unsafe-type-assertion': 'off',
4344
'@typescript-eslint/no-unsafe-unary-minus': 'off',
4445
'@typescript-eslint/non-nullable-type-assertion-style': 'off',
4546
'@typescript-eslint/only-throw-error': 'off',

packages/eslint-plugin/src/rules/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ import noUnsafeEnumComparison from './no-unsafe-enum-comparison';
8383
import noUnsafeFunctionType from './no-unsafe-function-type';
8484
import noUnsafeMemberAccess from './no-unsafe-member-access';
8585
import noUnsafeReturn from './no-unsafe-return';
86+
import noUnsafeTypeAssertion from './no-unsafe-type-assertion';
8687
import noUnsafeUnaryMinus from './no-unsafe-unary-minus';
8788
import noUnusedExpressions from './no-unused-expressions';
8889
import noUnusedVars from './no-unused-vars';
@@ -214,6 +215,7 @@ const rules = {
214215
'no-unsafe-function-type': noUnsafeFunctionType,
215216
'no-unsafe-member-access': noUnsafeMemberAccess,
216217
'no-unsafe-return': noUnsafeReturn,
218+
'no-unsafe-type-assertion': noUnsafeTypeAssertion,
217219
'no-unsafe-unary-minus': noUnsafeUnaryMinus,
218220
'no-unused-expressions': noUnusedExpressions,
219221
'no-unused-vars': noUnusedVars,
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import type { TSESTree } from '@typescript-eslint/utils';
2+
3+
import * as tsutils from 'ts-api-utils';
4+
import * as ts from 'typescript';
5+
6+
import {
7+
createRule,
8+
getConstrainedTypeAtLocation,
9+
getParserServices,
10+
isTypeAnyType,
11+
isTypeUnknownType,
12+
isUnsafeAssignment,
13+
} from '../util';
14+
15+
export default createRule({
16+
name: 'no-unsafe-type-assertion',
17+
meta: {
18+
type: 'problem',
19+
docs: {
20+
description: 'Disallow type assertions that narrow a type',
21+
requiresTypeChecking: true,
22+
},
23+
messages: {
24+
unsafeOfAnyTypeAssertion:
25+
'Unsafe cast from {{type}} detected: consider using type guards or a safer cast.',
26+
unsafeToAnyTypeAssertion:
27+
'Unsafe cast to {{type}} detected: consider using a more specific type to ensure safety.',
28+
unsafeTypeAssertion:
29+
"Unsafe type assertion: type '{{type}}' is more narrow than the original type.",
30+
},
31+
schema: [],
32+
},
33+
defaultOptions: [],
34+
create(context) {
35+
const services = getParserServices(context);
36+
const checker = services.program.getTypeChecker();
37+
38+
function getAnyTypeName(type: ts.Type): string {
39+
return tsutils.isIntrinsicErrorType(type) ? 'error typed' : '`any`';
40+
}
41+
42+
function isObjectLiteralType(type: ts.Type): boolean {
43+
return (
44+
tsutils.isObjectType(type) &&
45+
tsutils.isObjectFlagSet(type, ts.ObjectFlags.ObjectLiteral)
46+
);
47+
}
48+
49+
function checkExpression(
50+
node: TSESTree.TSAsExpression | TSESTree.TSTypeAssertion,
51+
): void {
52+
const expressionType = getConstrainedTypeAtLocation(
53+
services,
54+
node.expression,
55+
);
56+
const assertedType = getConstrainedTypeAtLocation(
57+
services,
58+
node.typeAnnotation,
59+
);
60+
61+
if (expressionType === assertedType) {
62+
return;
63+
}
64+
65+
// handle cases when casting unknown ==> any.
66+
if (isTypeAnyType(assertedType) && isTypeUnknownType(expressionType)) {
67+
context.report({
68+
node,
69+
messageId: 'unsafeToAnyTypeAssertion',
70+
data: {
71+
type: '`any`',
72+
},
73+
});
74+
75+
return;
76+
}
77+
78+
const unsafeExpressionAny = isUnsafeAssignment(
79+
expressionType,
80+
assertedType,
81+
checker,
82+
node.expression,
83+
);
84+
85+
if (unsafeExpressionAny) {
86+
context.report({
87+
node,
88+
messageId: 'unsafeOfAnyTypeAssertion',
89+
data: {
90+
type: getAnyTypeName(unsafeExpressionAny.sender),
91+
},
92+
});
93+
94+
return;
95+
}
96+
97+
const unsafeAssertedAny = isUnsafeAssignment(
98+
assertedType,
99+
expressionType,
100+
checker,
101+
node.typeAnnotation,
102+
);
103+
104+
if (unsafeAssertedAny) {
105+
context.report({
106+
node,
107+
messageId: 'unsafeToAnyTypeAssertion',
108+
data: {
109+
type: getAnyTypeName(unsafeAssertedAny.sender),
110+
},
111+
});
112+
113+
return;
114+
}
115+
116+
// Use the widened type in case of an object literal so `isTypeAssignableTo()`
117+
// won't fail on excess property check.
118+
const nodeWidenedType = isObjectLiteralType(expressionType)
119+
? checker.getWidenedType(expressionType)
120+
: expressionType;
121+
122+
const isAssertionSafe = checker.isTypeAssignableTo(
123+
nodeWidenedType,
124+
assertedType,
125+
);
126+
127+
if (!isAssertionSafe) {
128+
context.report({
129+
node,
130+
messageId: 'unsafeTypeAssertion',
131+
data: {
132+
type: checker.typeToString(assertedType),
133+
},
134+
});
135+
}
136+
}
137+
138+
return {
139+
'TSAsExpression, TSTypeAssertion'(
140+
node: TSESTree.TSAsExpression | TSESTree.TSTypeAssertion,
141+
): void {
142+
checkExpression(node);
143+
},
144+
};
145+
},
146+
});

packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-unsafe-type-assertion.shot

Lines changed: 33 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
0