-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
feat(eslint-plugin): replace ban-types with no-restricted-types, no-unsafe-function-type, no-wrapper-object-types #9102
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): replace ban-types with no-restricted-types, no-unsafe-function-type, no-wrapper-object-types #9102
Conversation
Thanks for the PR, @JoshuaKGoldberg! 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 75543e9. 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 1 targetSent with 💌 from NxCloud. |
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 don't like this name 😦 but couldn't think of one I really do like... no-primitive-class-wrappers
? no-class-wrapper-types
? So wordy, but I also like ending with -types
... Someone please help 🙏
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.
TL;DR ban-bad-built-in-class-types
or even just ban-bad-class-types
wordy? you must hate use-unknown-in-catch-callback-variable
😆
I guess the commonalities in this rule are, for type X flagged by the rule
- X is built-in ("built-in", "intrinsic")
- the types means "something which has X.prototype in its prototype chain". (gives us "class-type"/"object-type"/"prototype-type"/etc) <-- This makes the rule hard to name
- that type is bad for some reason, but not necessarily the same reason...
- in the case of
BigInt
,Boolean
,Number
,String
,Symbol
, because they refer to the boxed primitive, but the unboxed primitive is also assignable Object
, because it's terrible; any primitive is assignable, similar to previousFunction
, because it's unsound <-- This makes the rule hard to name- Technically we could argue all of these are unsound, but
Function
is really a difference in kind compared to the others, so it's hard to justify using "unsound"/"unsafe" in the name
- in the case of
- X is uppercase - technically true but not really the point 😄
So that would lead me to a rough template of {no/ban}-bad-{built-in/intrinsic}-{instance/prototype/class/object}-types
of which my tentative favorite is ban-bad-built-in-class-types
.
I'm not a fan of "wrapper" since Object
and Function
aren't wrappers. And same problem arises with using the word "primitive". If Function
weren't part of the rule though I think no-{boxed/wrapped}-primitive-type
would work nicely 🤷
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.
👋 What about no-ts-wrapper-types
?
Pro: These types are all defined by TypeScript itself, and the list of banned types not only includes primitive wrappers, but also wrappers for object
and a wide, any-ish function
which are technically not primitives per MDN. The name indicates it's related to a TypeScript feature itself like ban-ts-comment
does.
Con: Might be a little more confusing / need further explanation in the docs.
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.
wordy? you must hate
use-unknown-in-catch-callback-variable
😆
Haha, yes, I do! And now I'm realizing maybe we could have called it just use-unknown-in-catch-callback
. Ugh.
Long rule names make for less readable/pronouncable editor popups & CLI complaints. They're actively (mildly) worse for users.
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.
Some more suggestions from across the internet:
no-boxed-primitive-types
(@molisani)no-class-wraps
(@bumpylumps)no-class-wrapper-types
(@bennycode)no-object-instances-as-types
(@gangsthub)no-primitive-constructors
(@niklas-wortmann)no-primitive-wrappers
(@davidlj95)no-primordial-constructors
(@boneskull)no-upper-case-primitive-type
(@bradzacher)no-wrapper-types
(@jakebailey)no-wrapper-types
orno-primitive-wrapper-types
(@sandersn)prefer-lowercase-intrinsic-type
orprefer-intrinsic-type
(@Josh-Cena)prefer-primitive-type
(-thingy) (@the-ult)
Some general thoughts...
- "Primitive" would be something nice to include in there, but as @Josh-Cena pointed out,
object
isn't a primitive and so that's inaccurate - Moving
Function
bans into a separate rule (no-unsafe-call: ban calling Function #9108 (comment)) would help narrow down the naming- It'd be nice for naming to also move out
Object
bans into its own rule so we could then use "primitive" after all, but I don't know of any justifications on that one 😕
- It'd be nice for naming to also move out
- It's hard to justify using a term that isn't in common usage already. Words like "boxed" and "primordial" might (previously or presently) be good descriptors, but if they don't match MDN docs or common StackOverflow nomenclature, users will have a hard time with them
- We don't generally put abbreviations in rule names anymore. It's hard enough for users to understand lint rules, let alone have to remember random abbreviations in there too.
- I think it'd be nice to include some indication in the name that it's about types / type annotations, not generally using those things. E.g. ending with
-types
. That'd help disambiguate from other rules that do ban usage of some types (e.g.no-unsafe-*
forany
s).
That leaves a few finalists IMO:
no-class-wrapper-types
no-intrinsic-wrapper-types
no-intrinsic-class-wrapper-types
no-primitive-class-wrapper-types
prefer-lowercase-intrinsic-types
: if we split outFunction
into its own rule, since lower-casefunction
isn't a type
I'll continue to ruminate...
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 personally find the mention of "class" to be very odd, especially given pages like https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number and https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/Number do not contain that string.
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 can see how the term "class" could be confusing as it could be associated with limiting the behaviour in a real class
. While reading through the MDN docs, I came across this:
When properties are accessed on primitives, JavaScript auto-boxes the value into a wrapper object and accesses the property on that object instead. For example, "foo".includes("f") implicitly creates a String wrapper object and calls
String.prototype.includes()
on that object. This auto-boxing behavior is not observable in JavaScript code but is a good mental model of various behaviors
Source: https://developer.mozilla.org/en-US/docs/Glossary/Primitive
Stating the above, would these names be acceptable?
no-wrapper-object-types
no-auto-boxing-types
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.
@kirkwaiblinger what do you mean about
Object
not being a wrapper?
Oh, well, I think I meant the Object type doesn't conceptually refer to a wrapper around a primitive, though as you pointed out (and I wasn't aware) that is a subset of its possible uses. So, feel free to disregard my comment on that; my mind has been changed 🙂
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 like no-wrapper-object-types
a lot! "Wrapper" might be a bit general on its own, but the built-in upper-case types are the only ones I can think of that that jump to mind as "wrapper object types". Nice @bennycode!
Renaming now, but if anybody doesn't like this then please do continue the conversation. 🙁
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.
Update: following #9102 (comment), I split out a no-unsafe-function-type
. @Josh-Cena makes a good point that the function type isn't a wrapper.
That leaves the question of whether the no-wrapper-object-types
name is ok to include Object
. I.e. being ok with what @kirkwaiblinger mentioned: that it's not technically a wrapper itself. I'm in favor of allowing this: more convenience at the cost of slightly less pedantically correct rule name. But I could be convinced otherwise if we have a good rule name alternative and/or have justification to move Object
into its own rule (do we?).
packages/eslint-plugin/docs/rules/no-uppercase-wrapper-types.mdx
Outdated
Show resolved
Hide resolved
packages/eslint-plugin/docs/rules/no-uppercase-wrapper-types.mdx
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.
TL;DR ban-bad-built-in-class-types
or even just ban-bad-class-types
wordy? you must hate use-unknown-in-catch-callback-variable
😆
I guess the commonalities in this rule are, for type X flagged by the rule
- X is built-in ("built-in", "intrinsic")
- the types means "something which has X.prototype in its prototype chain". (gives us "class-type"/"object-type"/"prototype-type"/etc) <-- This makes the rule hard to name
- that type is bad for some reason, but not necessarily the same reason...
- in the case of
BigInt
,Boolean
,Number
,String
,Symbol
, because they refer to the boxed primitive, but the unboxed primitive is also assignable Object
, because it's terrible; any primitive is assignable, similar to previousFunction
, because it's unsound <-- This makes the rule hard to name- Technically we could argue all of these are unsound, but
Function
is really a difference in kind compared to the others, so it's hard to justify using "unsound"/"unsafe" in the name
- in the case of
- X is uppercase - technically true but not really the point 😄
So that would lead me to a rough template of {no/ban}-bad-{built-in/intrinsic}-{instance/prototype/class/object}-types
of which my tentative favorite is ban-bad-built-in-class-types
.
I'm not a fan of "wrapper" since Object
and Function
aren't wrappers. And same problem arises with using the word "primitive". If Function
weren't part of the rule though I think no-{boxed/wrapped}-primitive-type
would work nicely 🤷
packages/eslint-plugin/docs/rules/no-uppercase-wrapper-types.mdx
Outdated
Show resolved
Hide resolved
packages/eslint-plugin/docs/rules/no-uppercase-wrapper-types.mdx
Outdated
Show resolved
Hide resolved
packages/eslint-plugin/docs/rules/no-uppercase-wrapper-types.mdx
Outdated
Show resolved
Hide resolved
packages/eslint-plugin/docs/rules/no-uppercase-wrapper-types.mdx
Outdated
Show resolved
Hide resolved
packages/eslint-plugin/docs/rules/no-uppercase-wrapper-types.mdx
Outdated
Show resolved
Hide resolved
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.
@@ -15,6 +15,16 @@ It's often a good idea to ban certain types to help with consistency and safety. | |||
This rule bans specific types and can suggest alternatives. | |||
Note that it does not ban the corresponding runtime objects from being used. | |||
|
|||
:::danger Deprecated | |||
This rule is deprecated and now split across several rules: |
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.
Why are we deprecating the rule?
There's still value in the rule - just that the default config can be empty now.
People use this rule all the time to ban various types across their codebase. It's the only way to reliably target type names only without hitting value names.
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.
@bradzacher because it's essentially being renamed to no-restricted-types
, with the default values split into more targeted rules. I'll update the deprecation notice to make it more clear.
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.
Given this is a major - should we just remove the old rule name entirely?
Is there a benefit to keeping it if we're adding the new rules to recommended and the old rule should almost be a 1:1 name swap in config?
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 was thinking this is a pretty user-known rule so leaving it in would help with migration. The name swap might be 1:1 but the behavioral change means figuring out which of the three new rules individual config entries / inline comments map to.
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.
...heck, now that I post this, I'm liking deleting ban-types
and just leaving a tombstone page. cc @typescript-eslint/triage-team - we can always add back the old deleted rule if trying out on community repos surfaces a lot of pain (#9141), right?
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.
Makes sense to me too...
If we do that for this rule, should we just go ahead and do that for #8821, too, though?
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.
@kirkwaiblinger we can land #8821 against main and then remove it in v8
similarly we could technically land this PR in main and then remove ban-types
in v8 (which is what I originally suggested #8977 (comment)).
I.e. we could backport the new rules to v7 so they're available right now, deprecate ban-types
, and then do the actual breaking change (removing ban-types
) in v8.
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 like it. Once this PR goes in I can send a followup to backport the changes to v7 / main
, replacing the deletion with a deprecation.
} | ||
|
||
return { | ||
TSClassImplements(node): void { |
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.
Thought: should the two new rules check implements
and extends
? You can't extend number
or () => {}
so the use of Number
must either be intentional or refer to another type (since we still don't have scope analysis for this rule)
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.
Someone could, in theory, be doing:
extends
:interface MyFunction extends Function { __brand: 'my-function'; }
implements
:class Weird implements Function { apply: ...; ... }
.
...so for completeness, I think we need to?
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.
Is that a bad idea? If they do it there's no reason they are doing it by accident or ignorance.
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'm not strongly opposed to removing this for simplicity. It does feel a little weird to me to leave in a known thing that nobody should really ever do - even if it's only doable with great intent.
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.
Let's leave them in for now, but at least extends Function
feels fine to me.
Leaving my verbal approval, pending some discussion but not a blocker since we have that behavior already |
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.
Some nits and stuff but looks good to me 👍
Similarly, TypeScript's `Function` type refers to objects created with the `Function` constructor, i.e. `new Function("eval'd code with implicit args")`. | ||
`Function` allows being called with any number of arguments and returns type `any`. | ||
It's generally better to specify function parameter and return types with function type syntax. | ||
Using the primitives like `0` instead of object wrappers like `new Number(0)` is generally considered a JavaScript best practice. |
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.
For flow, you might link this and the next sentence with a conjunction such as "..., since ..." or something like that.
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 like the idea but can't think of phrasing I like here to go with it. Do you have a suggestion?
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.
@kirkwaiblinger I really want to get this in to unblock testing on community repos, so I'm merging now - but let's definitely revisit the docs before v8 goes stable!
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.
see #9363
packages/website/blog/2024-05-27-announcing-typescript-eslint-v8-beta.mdx
Outdated
Show resolved
Hide resolved
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.
🔥🔥
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.
My one feedback:
These two rules must use scope analysis to ensure the reference is to the global type, nor a local one.
Eg this code should be fine:
type T<Symbol> = Symbol;
type U<UU> = UU extends T<infer Function>
? Function : never;
Without scope analysis I believe the first case above will autofix to type T<Symbol> = symbol;
and break the code!
I say this because we specifically received this feedback in the past from users but had rejected it because "ban-types
is intentionally dumb and simple and just bans the name".
Given these rules are now targeted and toight - there's no reason we can't now add the scope analysis to be safe.
Is this all documented in the v8 blog post?
@@ -0,0 +1,22 @@ | |||
:::danger Deprecated |
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.
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.
Merging this now with roughly resolved conversations (except some more docs work) to unblock #9141. Note that this is into |
BREAKING CHANGE:
Replaces rules in the recommended configs.
PR Checklist
Overview
Finally deprecates the venerable
ban-types
rule and adds three new rules:no-restricted-types
: allowing banning a configurable list of type namesno-unsafe-function-type
: banning the built-inFunction
no-wrapper-object-types
: banningObject
and built-in class wrappers such asNumber
💖