Skip to content

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

Merged

Conversation

JoshuaKGoldberg
Copy link
Member

@JoshuaKGoldberg JoshuaKGoldberg commented May 15, 2024

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 names
  • 🆕 no-unsafe-function-type: banning the built-in Function
  • 🆕 no-wrapper-object-types: banning Object and built-in class wrappers such as Number

💖

@JoshuaKGoldberg JoshuaKGoldberg added the breaking change This change will require a new major version to be released label May 15, 2024
@JoshuaKGoldberg JoshuaKGoldberg added this to the 8.0.0 milestone May 15, 2024
@typescript-eslint
Copy link
Contributor

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.

Copy link

netlify bot commented May 15, 2024

Deploy Preview for typescript-eslint ready!

Name Link
🔨 Latest commit 75543e9
🔍 Latest deploy log https://app.netlify.com/sites/typescript-eslint/deploys/666ab8d63628680008c109fd
😎 Deploy Preview https://deploy-preview-9102--typescript-eslint.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 99 (no change from production)
Accessibility: 100 (no change from production)
Best Practices: 92 (no change from production)
SEO: 90 (no change from production)
PWA: 80 (no change from production)
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify site configuration.

Copy link

nx-cloud bot commented May 15, 2024

☁️ Nx Cloud Report

CI 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 target

Sent with 💌 from NxCloud.

Copy link
Member Author

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 🙏

Copy link
Member

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 previous
    • Function, 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
  • 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 🤷

Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Member Author

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:

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'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-* for anys).

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 out Function into its own rule, since lower-case function isn't a type

I'll continue to ruminate...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link

@bennycode bennycode May 24, 2024

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

Copy link
Member

@kirkwaiblinger kirkwaiblinger May 25, 2024

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 🙂

Copy link
Member Author

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. 🙁

Copy link
Member Author

@JoshuaKGoldberg JoshuaKGoldberg Jun 2, 2024

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?).

@JoshuaKGoldberg JoshuaKGoldberg requested a review from a team May 15, 2024 23:57
Copy link
Member

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 previous
    • Function, 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
  • 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 🤷

Co-authored-by: Kirk Waiblinger <kirk.waiblinger@gmail.com>
@JoshuaKGoldberg JoshuaKGoldberg changed the title feat(eslint-plugin): add no-restricted-types, no-uppercase-alias-types rules feat(eslint-plugin): add no-restricted-types, no-wrapper-object-types rules May 26, 2024
Copy link
Member

@kirkwaiblinger kirkwaiblinger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's all coming together

@kirkwaiblinger kirkwaiblinger added the 1 approval >=1 team member has approved this PR; we're now leaving it open for more reviews before we merge label May 27, 2024
@@ -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:
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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?

Copy link
Member Author

@JoshuaKGoldberg JoshuaKGoldberg May 27, 2024

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.

Copy link
Member Author

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?

Copy link
Member

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?

Copy link
Member

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.

Copy link
Member Author

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.

@JoshuaKGoldberg JoshuaKGoldberg changed the title feat(eslint-plugin): add no-restricted-types, no-wrapper-object-types rules feat(eslint-plugin): replace ban-types with no-restricted-types, no-wrapper-object-types rules May 27, 2024
}

return {
TSClassImplements(node): void {
Copy link
Member

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)

Copy link
Member Author

@JoshuaKGoldberg JoshuaKGoldberg Jun 10, 2024

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?

Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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.

@JoshuaKGoldberg JoshuaKGoldberg requested review from Josh-Cena, kirkwaiblinger and bradzacher and removed request for kirkwaiblinger June 3, 2024 11:33
@JoshuaKGoldberg JoshuaKGoldberg removed the 1 approval >=1 team member has approved this PR; we're now leaving it open for more reviews before we merge label Jun 3, 2024
@Josh-Cena
Copy link
Member

Leaving my verbal approval, pending some discussion but not a blocker since we have that behavior already

Copy link
Member

@kirkwaiblinger kirkwaiblinger left a 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.
Copy link
Member

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.

Copy link
Member Author

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?

Copy link
Member Author

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!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see #9363

JoshuaKGoldberg and others added 2 commits June 12, 2024 03:52
Co-authored-by: Kirk Waiblinger <kirk.waiblinger@gmail.com>
Copy link
Member

@kirkwaiblinger kirkwaiblinger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥🔥

Copy link
Member

@bradzacher bradzacher left a 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.


Otherwise LGTM!
sobeautiful.gif


Is this all documented in the v8 blog post?

@@ -0,0 +1,22 @@
:::danger Deprecated
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

press-f-pay-respect.gif

Copy link
Member

@bradzacher bradzacher left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JoshuaKGoldberg
Copy link
Member Author

JoshuaKGoldberg commented Jun 13, 2024

Merging this now with roughly resolved conversations (except some more docs work) to unblock #9141.

Note that this is into v8, not main - we can definitely clean things up & make any changes we need in v8 as needed before this goes stable!

@JoshuaKGoldberg JoshuaKGoldberg merged commit 5cd80ca into typescript-eslint:v8 Jun 13, 2024
59 checks passed
@JoshuaKGoldberg JoshuaKGoldberg deleted the no-ban-types-splitup branch June 13, 2024 09:34
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jun 25, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
breaking change This change will require a new major version to be released
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants