Skip to content

feat(eslint-plugin): new rule no-unsafe-type-assertion #10051

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
8f38cec
initial implementation
ronami Sep 24, 2024
037978c
tests
ronami Sep 24, 2024
a50625b
docs
ronami Sep 24, 2024
15ce7f8
more tests
ronami Sep 24, 2024
7c44606
use checker.typeToString() over getTypeName()
ronami Sep 24, 2024
cc4ea8b
use link
ronami Sep 25, 2024
e31cf39
oops
ronami Sep 25, 2024
8a8965f
add tests
ronami Sep 28, 2024
5bbe0c9
Merge branch 'main' into no-unsafe-type-assertion
ronami Sep 28, 2024
f0c429e
Merge branch 'main' into no-unsafe-type-assertion
ronami Oct 11, 2024
08577a8
remove unnecessary typescript 5.4 warning
ronami Oct 11, 2024
2ab1c87
adjust format to new rules
ronami Oct 11, 2024
8f29e89
Merge branch 'main' into no-unsafe-type-assertion
ronami Oct 15, 2024
a5dbd5b
update error message to be more concise
ronami Oct 11, 2024
ff73c4d
match implementation to be inline with no-unsafe-* rules
ronami Oct 15, 2024
913529f
rework tests
ronami Oct 15, 2024
88d15de
refactor
ronami Oct 15, 2024
c4f7ce7
update snapshots
ronami Oct 15, 2024
c34f7c1
fix error message showing original type instead of asserted type
ronami Oct 25, 2024
e93cca5
update snapshots
ronami Oct 25, 2024
1d32280
add a warning for object stubbing on test files
ronami Oct 25, 2024
d4c5236
fix linting
ronami Oct 25, 2024
03ae3bc
adjust test to lint fixes
ronami Oct 25, 2024
9980c9e
simplify type comparison
ronami Nov 10, 2024
8b0912f
rework code-comments and rename variables
ronami Nov 10, 2024
07ae890
rework the opening paragraph to make it more beginner-friendly
ronami Nov 10, 2024
5d8c521
Update packages/eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx
ronami Nov 11, 2024
033fd0b
fix: narrow/widen in description
JoshuaKGoldberg Nov 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions packages/eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
description: 'Disallow type assertions that narrow a type.'
---

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

> 🛑 This file is source code, not the primary documentation location! 🛑
>
> See **https://typescript-eslint.io/rules/no-unsafe-type-assertion** for documentation.

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.

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.

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.

## Examples

<Tabs>
<TabItem value="❌ Incorrect">

```ts
function f() {
return Math.random() < 0.5 ? 42 : 'oops';
}

const z = f() as number;

const items = [1, '2', 3, '4'];

const number = items[0] as number;
```

</TabItem>
<TabItem value="✅ Correct">

```ts
function f() {
return Math.random() < 0.5 ? 42 : 'oops';
}

const z = f() as number | string | boolean;

const items = [1, '2', 3, '4'];

const number = items[0] as number | string | undefined;
```

</TabItem>
</Tabs>

## When Not To Use It

If your codebase has many unsafe type assertions, then it may be difficult to enable this rule.
It may be easier to skip the `no-unsafe-*` rules pending increasing type safety in unsafe areas of your project.
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.

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.

## Further Reading

- More on TypeScript's [type assertions](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-assertions)
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/configs/all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export = {
'@typescript-eslint/no-unsafe-function-type': 'error',
'@typescript-eslint/no-unsafe-member-access': 'error',
'@typescript-eslint/no-unsafe-return': 'error',
'@typescript-eslint/no-unsafe-type-assertion': 'error',
'@typescript-eslint/no-unsafe-unary-minus': 'error',
'no-unused-expressions': 'off',
'@typescript-eslint/no-unused-expressions': 'error',
Expand Down
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/configs/disable-type-checked.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export = {
'@typescript-eslint/no-unsafe-enum-comparison': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/no-unsafe-type-assertion': 'off',
'@typescript-eslint/no-unsafe-unary-minus': 'off',
'@typescript-eslint/non-nullable-type-assertion-style': 'off',
'@typescript-eslint/only-throw-error': 'off',
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import noUnsafeEnumComparison from './no-unsafe-enum-comparison';
import noUnsafeFunctionType from './no-unsafe-function-type';
import noUnsafeMemberAccess from './no-unsafe-member-access';
import noUnsafeReturn from './no-unsafe-return';
import noUnsafeTypeAssertion from './no-unsafe-type-assertion';
import noUnsafeUnaryMinus from './no-unsafe-unary-minus';
import noUnusedExpressions from './no-unused-expressions';
import noUnusedVars from './no-unused-vars';
Expand Down Expand Up @@ -213,6 +214,7 @@ const rules = {
'no-unsafe-function-type': noUnsafeFunctionType,
'no-unsafe-member-access': noUnsafeMemberAccess,
'no-unsafe-return': noUnsafeReturn,
'no-unsafe-type-assertion': noUnsafeTypeAssertion,
'no-unsafe-unary-minus': noUnsafeUnaryMinus,
'no-unused-expressions': noUnusedExpressions,
'no-unused-vars': noUnusedVars,
Expand Down
146 changes: 146 additions & 0 deletions packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import type { TSESTree } from '@typescript-eslint/utils';

import * as tsutils from 'ts-api-utils';
import * as ts from 'typescript';

import {
createRule,
getConstrainedTypeAtLocation,
getParserServices,
isTypeAnyType,
isTypeUnknownType,
isUnsafeAssignment,
} from '../util';

export default createRule({
name: 'no-unsafe-type-assertion',
meta: {
type: 'problem',
docs: {
description: 'Disallow type assertions that narrow a type',
requiresTypeChecking: true,
},
messages: {
unsafeOfAnyTypeAssertion:
'Unsafe cast from {{type}} detected: consider using type guards or a safer cast.',
unsafeToAnyTypeAssertion:
'Unsafe cast to {{type}} detected: consider using a more specific type to ensure safety.',
unsafeTypeAssertion:
"Unsafe type assertion: type '{{type}}' is more narrow than the original type.",
},
schema: [],
},
defaultOptions: [],
create(context) {
const services = getParserServices(context);
const checker = services.program.getTypeChecker();

function getAnyTypeName(type: ts.Type): string {
return tsutils.isIntrinsicErrorType(type) ? 'error typed' : '`any`';
}

function isObjectLiteralType(type: ts.Type): boolean {
return (
tsutils.isObjectType(type) &&
tsutils.isObjectFlagSet(type, ts.ObjectFlags.ObjectLiteral)
);
}

function checkExpression(
node: TSESTree.TSAsExpression | TSESTree.TSTypeAssertion,
): void {
const expressionType = getConstrainedTypeAtLocation(
services,
node.expression,
);
const assertedType = getConstrainedTypeAtLocation(
services,
node.typeAnnotation,
);

if (expressionType === assertedType) {
return;
}

// handle cases when casting unknown ==> any.
if (isTypeAnyType(assertedType) && isTypeUnknownType(expressionType)) {
context.report({
node,
messageId: 'unsafeToAnyTypeAssertion',
data: {
type: '`any`',
},
});

return;
}

const unsafeExpressionAny = isUnsafeAssignment(
expressionType,
assertedType,
checker,
node.expression,
);

if (unsafeExpressionAny) {
context.report({
node,
messageId: 'unsafeOfAnyTypeAssertion',
data: {
type: getAnyTypeName(unsafeExpressionAny.sender),
},
});

return;
}

const unsafeAssertedAny = isUnsafeAssignment(
assertedType,
expressionType,
checker,
node.typeAnnotation,
);

if (unsafeAssertedAny) {
context.report({
node,
messageId: 'unsafeToAnyTypeAssertion',
data: {
type: getAnyTypeName(unsafeAssertedAny.sender),
},
});

return;
}

// Use the widened type in case of an object literal so `isTypeAssignableTo()`
// won't fail on excess property check.
const nodeWidenedType = isObjectLiteralType(expressionType)
? checker.getWidenedType(expressionType)
: expressionType;

const isAssertionSafe = checker.isTypeAssignableTo(
nodeWidenedType,
assertedType,
);

if (!isAssertionSafe) {
context.report({
node,
messageId: 'unsafeTypeAssertion',
data: {
type: checker.typeToString(assertedType),
},
});
}
}

return {
'TSAsExpression, TSTypeAssertion'(
node: TSESTree.TSAsExpression | TSESTree.TSTypeAssertion,
): void {
checkExpression(node);
},
};
},
});

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading