-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
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
feat(eslint-plugin): new rule no-unsafe-type-assertion
#10051
feat(eslint-plugin): new rule no-unsafe-type-assertion
#10051
Conversation
Thanks for the PR, @ronami! typescript-eslint is a 100% community driven project, and we are incredibly grateful that you are contributing to that community. The core maintainers work on this in their personal time, so please understand that it may not be possible for them to review your work immediately. Thanks again! 🙏 Please, if you or your company is finding typescript-eslint valuable, help us sustain the project by sponsoring it transparently on https://opencollective.com/typescript-eslint. |
✅ Deploy Preview for typescript-eslint ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
☁️ Nx Cloud ReportCI is running/has finished running commands for commit 033fd0b. As they complete they will appear below. Click to see the status, the terminal output, and the build insights. 📂 See all runs for this CI Pipeline Execution ✅ Successfully ran 2 targetsSent with 💌 from NxCloud. |
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #10051 +/- ##
==========================================
+ Coverage 86.18% 86.62% +0.44%
==========================================
Files 428 433 +5
Lines 14989 15193 +204
Branches 4345 4434 +89
==========================================
+ Hits 12918 13161 +243
+ Misses 1725 1675 -50
- Partials 346 357 +11
Flags with carried forward coverage won't be shown. Click here to find out more.
|
9d898f9
to
7c44606
Compare
packages/eslint-plugin/tests/rules/no-unsafe-type-assertion.test.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work on a first pass! And, I'm personally super excited for this to be getting worked on. 🙂
Looking forward to seeing where the discussions on all the complicated edge cases go! Hopefully I've given you plenty to think about 😁
packages/eslint-plugin/tests/rules/no-unsafe-type-assertion.test.ts
Outdated
Show resolved
Hide resolved
packages/eslint-plugin/tests/rules/no-unsafe-type-assertion.test.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with Kirk 🙂
Love how straightforward this implementation is... and it pains me to say we might need to make it more complicated.
}, | ||
messages: { | ||
unsafeTypeAssertion: | ||
'Unsafe type assertion: type `{{type}}` is not assignable to type `{{asserted}}`', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thinking on the long error messages: we haven't had to really solve this problem before, which is why we don't have good helpers. Most of our error messages either don't mention types or only refer to small names such as identifiers.
For this rule, I think we can generally assume that the explicitly written type should be simple enough to report with. What do you think about this?
Unsafe type assertion: type '{{type}}' is more narrow than the original type.
I don't love "original type" but 🤷 can't think of anything nicer...
packages/eslint-plugin/tests/rules/no-unsafe-type-assertion.test.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(wrong button...)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just posting a thought while https://github.com/typescript-eslint/typescript-eslint/pull/10051/files#r1828085145 is pending.
> | ||
> See **https://typescript-eslint.io/rules/no-unsafe-type-assertion** for documentation. | ||
|
||
This rule forbids using type assertions to narrow a type, as it can lead to unsafe assumptions that bypass TypeScript’s type-checking. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unsafe assumptions
What unsafe assumptions?
Someone reading this who isn't already familiar with the concepts wouldn't likely understand what it means. I think it'd even be easy to misinterpret this as saying all type assertions are unsafe.
Could you add a section to this page explaining this for someone who doesn't already understand?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I definitely agree! I've changed/expanded the opening paragraph to explain this better. Wdyt?
Thanks @JoshuaKGoldberg; I'll update this section in a bit. To make sure, is https://github.com/typescript-eslint/typescript-eslint/pull/10051/files#r1828085145 pending for something from me? |
Last I checked it was requesting more test coverage. If that's done then we can re-review. 👍 |
The PR has gone through several changes since then (with the help of @kirkwaiblinger ❤️). Other than your recent request, I think the PR is ready for a re-review. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🙌
if ( | ||
isTypeFlagSet(uncast, ts.TypeFlags.Undefined) && | ||
isTypeFlagSet(cast, ts.TypeFlags.Undefined) && | ||
tsutils.isCompilerOptionEnabled( | ||
compilerOptions, | ||
'exactOptionalPropertyTypes', | ||
) | ||
) { | ||
const uncastParts = tsutils | ||
.unionTypeParts(uncast) | ||
.filter(part => !isTypeFlagSet(part, ts.TypeFlags.Undefined)); | ||
|
||
const castParts = tsutils | ||
.unionTypeParts(cast) | ||
.filter(part => !isTypeFlagSet(part, ts.TypeFlags.Undefined)); | ||
|
||
if (uncastParts.length !== castParts.length) { | ||
return false; | ||
} | ||
|
||
const uncastPartsSet = new Set(uncastParts); | ||
return castParts.every(part => uncastPartsSet.has(part)); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think if there's a test case that passes only with this logic, then yeah, let's use it. Otherwise just a ===
is sufficient IMO.
Great extract+reuse!
I think that's all resolved! 👍 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Co-authored-by: Kirk Waiblinger <kirk.waiblinger@gmail.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whoohoo! Thanks!
Really excited to finally have a meaningful rule around unsafe assertions in. This has been a sore spot for so long.
033fd0b
…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>
##### [v8.15.0](https://github.com/typescript-eslint/typescript-eslint/blob/HEAD/packages/eslint-plugin/CHANGELOG.md#8150-2024-11-18) ##### 🚀 Features - **eslint-plugin:** \[prefer-nullish-coalescing] fix detection of `ignoreConditionalTests` involving boolean `!` operator ([#10299](typescript-eslint/typescript-eslint#10299)) - **eslint-plugin:** new rule `no-unsafe-type-assertion` ([#10051](typescript-eslint/typescript-eslint#10051)) - **eslint-plugin:** added related-getter-setter-pairs rule ([#10192](typescript-eslint/typescript-eslint#10192)) ##### 🩹 Fixes - **utils:** add defaultOptions to meta in rule ([#10339](typescript-eslint/typescript-eslint#10339)) - **eslint-plugin:** report deprecations used in default export ([#10330](typescript-eslint/typescript-eslint#10330)) - **eslint-plugin:** \[explicit-module-boundary-types] and \[explicit-function-return-type] don't report on `as const satisfies` ([#10315](typescript-eslint/typescript-eslint#10315)) - **eslint-plugin:** \[await-thenable, return-await] don't flag awaiting unconstrained type parameter as unnecessary ([#10314](typescript-eslint/typescript-eslint#10314)) - **eslint-plugin:** \[consistent-indexed-object-style] handle circular mapped types ([#10301](typescript-eslint/typescript-eslint#10301)) ##### ❤️ Thank You - Josh Goldberg ✨ - Kim Sang Du [@developer-bandi](https://github.com/developer-bandi) - Luis Sebastian Urrutia Fuentes [@LuisUrrutia](https://github.com/LuisUrrutia) - Phillip Huang - Ronen Amiel - Szydlak [@wszydlak](https://github.com/wszydlak) You can read about our [versioning strategy](https://main--typescript-eslint.netlify.app/users/versioning) and [releases](https://main--typescript-eslint.netlify.app/users/releases) on our website.
| datasource | package | from | to | | ---------- | -------------------------------- | ------ | ------ | | npm | @typescript-eslint/eslint-plugin | 8.14.0 | 8.15.0 | | npm | @typescript-eslint/parser | 8.14.0 | 8.15.0 | ## [v8.15.0](https://github.com/typescript-eslint/typescript-eslint/blob/HEAD/packages/eslint-plugin/CHANGELOG.md#8150-2024-11-18) ##### 🚀 Features - **eslint-plugin:** \[prefer-nullish-coalescing] fix detection of `ignoreConditionalTests` involving boolean `!` operator ([#10299](typescript-eslint/typescript-eslint#10299)) - **eslint-plugin:** new rule `no-unsafe-type-assertion` ([#10051](typescript-eslint/typescript-eslint#10051)) - **eslint-plugin:** added related-getter-setter-pairs rule ([#10192](typescript-eslint/typescript-eslint#10192)) ##### 🩹 Fixes - **utils:** add defaultOptions to meta in rule ([#10339](typescript-eslint/typescript-eslint#10339)) - **eslint-plugin:** report deprecations used in default export ([#10330](typescript-eslint/typescript-eslint#10330)) - **eslint-plugin:** \[explicit-module-boundary-types] and \[explicit-function-return-type] don't report on `as const satisfies` ([#10315](typescript-eslint/typescript-eslint#10315)) - **eslint-plugin:** \[await-thenable, return-await] don't flag awaiting unconstrained type parameter as unnecessary ([#10314](typescript-eslint/typescript-eslint#10314)) - **eslint-plugin:** \[consistent-indexed-object-style] handle circular mapped types ([#10301](typescript-eslint/typescript-eslint#10301)) ##### ❤️ Thank You - Josh Goldberg ✨ - Kim Sang Du [@developer-bandi](https://github.com/developer-bandi) - Luis Sebastian Urrutia Fuentes [@LuisUrrutia](https://github.com/LuisUrrutia) - Phillip Huang - Ronen Amiel - Szydlak [@wszydlak](https://github.com/wszydlak) You can read about our [versioning strategy](https://main--typescript-eslint.netlify.app/users/versioning) and [releases](https://main--typescript-eslint.netlify.app/users/releases) on our website.
PR Checklist
Overview
This PR addresses #7173 and introduces the new
no-unsafe-type-assertion
rule for using type assertions in a type-safe way.The rule forbids using type assertions to narrow a type:
Instead, it only supports type casting for widening a type:
Things to consider
Non null assertions
Non-null assertions are a way to narrow down a type, as long as the type includes
null
orundefined
:Reporting non-null assertions can create a lot of noise, especially on nested member expressions. Since there's already a rule that forbids non-null assertions, I've decided not to report these a second time.
Non-null assertions can be written differently as a type-cast (the two examples are semantically the same, and
non-nullable-type-assertion-style
will auto-fix between the two):Since
no-non-null-assertion
doesn't cover the type-cast way, this rule will report it as unsafe, even though it's essentially the same as the non-null operator (which won't be reported).This does mean, though, that usage like this will be reported by both this rule and
non-nullable-type-assertion-style
:Possibly long lint errors
The current implementation displays the full names of both the type and the asserted type. If any of them is a big union, it can create long error messages:
While some of them can be reduced (by mimicking TypeScript's errors and only showing the most specific type that doesn't match), some error messages may be truncated.
In some cases, this can get way out of hand. For example, this type assertion has the following lint error:
I think this is too verbose, but I see many other rules using similar logic to display type names.
What's your opinion on this?
Testing this on a large project
I've run the rule locally on
@typescript-eslint
's repo. There are 98 failures; most seem valid, but I found some issues. Most issues I've encountered are related to how straightforward (or unclear) the error message is rather than its correctness.I wrote about long error messages, but I think it's worth discussing how types are displayed.
The following example:
Will fail with:
Types are referenced by their name (same for type constraints) rather than explaining why these types don't match.
I'll be happy to hear your opinions on this.
Forbids a common pattern
A common (though unsafe) pattern is to cast a value to
unknown,
followed by whatever type it should be:This escape hatch lets developers cast a value explicitly to something else, knowing it's unsafe.
I think it's reasonable to have an option not to report such cases so the developer doesn't have to use an eslint-ignore comment in addition to a very verbose assertion.
Relies on TypeScript 5.4 or above
This rule relies on microsoft/TypeScript#56448, which made
isTypeAssignableTo()
public and can't be used with earlier versions of TypeScript.I added a note about this in the docs, but I don't know if there should be a runtime check along with it. I couldn't find anything on similar rules.