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): split no-empty-object-type rule out from ban-types rule #8977
base: v8
Are you sure you want to change the base?
feat(eslint-plugin): split no-empty-object-type rule out from ban-types rule #8977
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. |
@@ -82,32 +85,6 @@ const defaultTypes: Types = { | |||
'If you are expecting the function to accept certain arguments, you should explicitly define the function shape.', | |||
].join('\n'), | |||
}, | |||
|
|||
// object typing | |||
Object: { |
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.
We didn't explicitly discuss Object
, but I think it makes sense in the spirit of this change to switch it to being treated as another banned uppercase alias. I've never seen people confuse Object
for {}
.
## Further Reading | ||
|
||
- [Enhancement: [ban-types] Split the {} ban into a separate, better-phrased rule](https://github.com/typescript-eslint/typescript-eslint/issues/8700) | ||
- [The Empty Object Type in TypeScript](https://www.totaltypescript.com/the-empty-object-type-in-typescript) |
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.
FYI @mattpocock. 👋
Fun fact: this is the first time we're linking to totaltypescript.com on typescript-eslint.io... I'm honestly surprised it took this long! If there are other articles you think would go well in Further Reading sections for rules, we'd be very up for considering adding them.
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.
Thanks pal, much appreciated!
Co-authored-by: Kirk Waiblinger <53019676+kirkwaiblinger@users.noreply.github.com>
Co-authored-by: Kirk Waiblinger <53019676+kirkwaiblinger@users.noreply.github.com>
Mildly tangential - but I can envision the following options:
|
This comment was marked as resolved.
This comment was marked as resolved.
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## v8 #8977 +/- ##
==========================================
- Coverage 87.35% 87.34% -0.01%
==========================================
Files 386 387 +1
Lines 13019 13025 +6
Branches 3769 3769
==========================================
+ Hits 11373 11377 +4
- Misses 1355 1356 +1
- Partials 291 292 +1
Flags with carried forward coverage won't be shown. Click here to find out 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.
@bradzacher threading #8977 (comment):
allowInTypeAliasWithName
- for react codebases to allowtype MyProps = {}
Isn't that a code smell? Why would they declare an empty props type? The only reason I can think of is copypasta / using a template, but it's "dead" code at that point.
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.
@JoshuaKGoldberg It happens if you are using forwardRef
and your component takes a second ref
arg. Of course props: unknown
would also work if you are not using it anyway...
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.
You have two choices - declare nothing at all function Component() {}
or declare an empty something function Component(_: Props) {}
A lot of people like doing the latter as it gives them an extension point to build from - eg if they know for sure they are going to add props later.
Forward refs is another good example of when it's hard required.
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 wouldn't you either omit the props type for forwardRef
or use object
?
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 wouldn't you either omit the props type for
forwardRef
Because I use the following style:
function BaseComp(props: {}, ref: ForwardedRef<HTMLSpanElement>) {
// ...
}
const Comp = forwardRef(BaseComp);
So I have to provide types.
use
object
As I said unknown
or object
both work, but {}
is not wrong, and best describes the intent (an object with more properties to be added).
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.
best describes the intent (an object with more properties to be added).
But it doesn't describe just objects, right? It's an object or non-nullish primitive? I think in this case what we're seeing is the rule acting as it should: telling folks not to use {}
because it also allows those primitives.
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.
In the case of a react component - the type of the first argument is intersected with the standard framework provided JSX props by TS at the JSX "callsite". This is done automatically by TS - it intersects with global.JSX.IntrinsicAttributes
IIRC.
EG if you defined function C(props: P) {}
then render <C />
- the props you're allowed to pass to C
are P & global.JSX.IntrinsicAttributes
.
AFAIK all "JSX" frameworks define at least { key?: string }
so the resulting type will ALWAYS be an object and not a primitive.
Additionally it's JSX - you can pass nothing but an object. JSX attributes are syntactic sugar for an object and a JSX spread attribute is enforced by TS to be an object.
So yeah you could do function C(_: unknown, ref: ForwardedRef<x>) {}
- but that would be more verbose and more annoying than using _: {}
or _: Props
.
I'm all for us continuing to report on _: {}
- but it's very common to do
type CompProps = {};
function Comp(_: Props, ref: X) {}
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 still unconvinced 😄. The CompProps
example is IMO even more in-violation because it (a) suggests doing that kind of type (= {}
) is ok and (b) is something that can be reused outside the component. The fact that the framework / its types make the types safe is nice, but doesn't stop folks from reusing the dangerous type aliases outside the framework.
let myPropsToBeUsedLater: CompProps;
myPropsToBeUsedLater = "oops";
Is there maybe a more targeted option we could go for than the broad allowInTypeAliasWithName
?
If someone explicitly says "Josh, we're in majority, we'd like to override your veto, please implement this" then I won't be upset at all. Nor would I be upset at all if you just push to this branch 🙂. Either way I'm expecting this rule to be one of the ones that gets a lot of scrutiny in the testing period for v8.
|
||
## Related To | ||
|
||
- [`no-empty-interface`](./no-empty-interface.mdx) |
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 feel like these two rules are really one, maybe as an option allowInterface
for declaration merging
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.
Hmm, I see them as separate rules. They're definitely related but I think the justifications for disabling either are very different.
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.
interface T {}
is not any different from type T = {}
by itself, though. The motivation says:
An empty interface in TypeScript does very little: any non-nullable value is assignable to
{}
. Using an empty interface is often a sign of programmer error, such as misunderstanding the concept of{}
or forgetting to fill in fields.
That is exactly applicable to empty object types. There is only slightly more reason to disable no-empty-interface
because it allows declaration merging.
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.
Interesting. I'll need to think on that a bit... Just confirming, you're suggesting we should merge no-empty-interface
into this rule for v8, too?
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.
Yes, exactly.
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.
the difference between the two is that no-empty-interface
allows you to ban interface Foo extends Bar {}
in favour of type Foo = Bar
.
However a similar rule doesn't make sense for type aliases.
But there's no reason we couldn't merge them and keep the allowSingleExtends
option just for interfaces.
but you're JoshG isn't wrong - the scope is different because the current form bans {}
anywhere - not just in type
aliases.
I personally can't think of a reason you'd ever want to have one banned, but not the other.
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.
Heh, I don't have a strong preference either way, and it sounds like the current consensus is leaning towards unification? Unless @kirkwaiblinger has strong input I'm up for unifying.
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.
no strong opinions on this!
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 will say, though, it does feel like yet another breaking change for users just to preserve the existing behavior. So, I guess "no strong opinion" defaults to not merging in this case 🤷
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.
it does feel like yet another breaking change for users just to preserve the existing behavior
I would have said the opposite - it's a breaking change TO merge.
Cos if you don't merge - people are just switching from ban-types
to this new rule.
But if you merge they ALSO need to switch from no-empty-interface
to this new rule.
Co-authored-by: Joshua Chen <sidachen2003@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.
Big picture here looks good to me. Added a few more questions and suggestion, but basically consider this a thumbs up from me 👍
I want to say that I really like how you managed to keep the doc page concise on this nuanced topic, and encouraging about disabling if it helps more than hurts.
✅ Deploy Preview for typescript-eslint ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
Also worth noting that we could land this as a standalone rule for v7 and then in v8 we change the defaults and make this new rule recommended. That would allow us to do a deprecation of |
|
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.
general logic LGTM
BREAKING CHANGE:
Changes the default options of a lint rule
PR Checklist
Overview
Per #8700 (comment), splits out the banning of
{}
from theban-types
rule into a newno-empty-object-type
rule.