8000 Rule proposal: restrict-constructor-expressions · Issue #10895 · typescript-eslint/typescript-eslint · GitHub
[go: up one dir, main page]

Skip to content

Rule proposal: restrict-constructor-expressions #10895

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

Open
6 tasks done
karlhorky opened this issue Feb 27, 2025 · 15 comments
Open
6 tasks done

Rule proposal: restrict-constructor-expressions #10895

karlhorky opened this issue Feb 27, 2025 · 15 comments
Labels
enhancement: new plugin rule New rule request for eslint-plugin evaluating community engagement we're looking for community engagement on this issue to show that this problem is widely important package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin

Comments

@karlhorky
Copy link
karlhorky commented Feb 27, 2025

Before You File a Proposal Please Confirm You Have Done The Following...

My proposal is suitable for this project

  • My proposal specifically checks TypeScript syntax, or it proposes a check that requires type information to be accurate.
  • My proposal is not a "formatting rule"; meaning it does not just enforce how code is formatted (whitespace, brace placement, etc).
  • I believe my proposal would be useful to the broader TypeScript community (meaning it is not a niche proposal).

Description

Similar to restrict-template-expressions, the new rule restrict-constructor-expressions restrict the types of expressions passed to constructors such as Number() and String() to a subset which result in non-surprising return values

TypeScript types for constructors have a permissive any type for the parameters:

TypeScript playground showing that the Number constructor has a parameter type of 'any'
TypeScript playground showing that the String constructor has a parameter type of 'any'

Fail Cases

Number({stdout: '123'}) // NaN
Number(['2']) // 2
String({result: 123}) // '[object Object]'

// I'm guessing there are many more...

Pass Cases

Number({stdout: '123'}.stdout) // 123
Number(['2'][0]) // 2
String({result: 123}.result) // '123'

Additional Info

No response

@karlhorky karlhorky added enhancement: new plugin rule New rule request for eslint-plugin package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin triage Waiting for team members to take a look labels Feb 27, 2025
@kirkwaiblinger
Copy link
Member

Related: #10862, #4314

@bradzacher
Copy link
Member
bradzacher commented Feb 27, 2025

I can't search right now but this came up not long ago and I vaguely remember the team was broadly aligned that such a rule was niche and not broadly applicable enough to need its own rule.

  • The String case can be handled by no-base-to-string
  • The Boolean case we generally ignore as an intentional escape hatch for rules (eg strict-boolean-expressions ignores it)
  • The Number case is the "looser" form of Number.parseInt/Float and (generally) isn't something people should use and should be banned via no-restricted-globals or similar.
  • The Symbol case doesn't need restricting.

@Josh-Cena
Copy link
Member

I disagree that Number should be banned, but I agree that this rule doesn't seem all that useful in general. It seems that the motivation is ultimately "ban arbitrary coercion to number", but I doubt that happens a lot.

@karlhorky
Copy link
Author

Our use case was this code:

const postgresUid = Number(await execa`id -u postgres`);

This was originally working, a few execa versions ago. But in one of the breaking changes, they started returning an object with .stdout property.

Fixed code was:

const postgresUid = Number((await execa`id -u postgres`).stdout);

It was surprising to me that no types or linting were preventing this Number(obj) pattern - seems like somethi 8000 ng that could happen to people a lot, similar to the String() case.

@Josh-Cena
Copy link
Member

I suppose you could do it with no-deprecated and some declaration merging to deprecate Number(any) and require Number(string) instead. Haven't tried though.

@bradzacher
Copy link
Member

I'd personally just ban Number and force people to use parseInt/parseFloat which iirc have better types.

@karlhorky
Copy link
Author

I suppose you could do it with no-deprecated and some declaration merging to deprecate Number(any) and require Number(string) instead. Haven't tried though.

Yeah, I guess I will come up with my own home-grown solution to lint / type-check for Number(nonString) and maybe also String(nonSerializable).

Too bad that typescript-eslint won't add a no-base-to-string-symmetrical rule for Number(), I think these problems are probably not uncommon.

I'd personally just ban Number and force people to use parseInt/parseFloat which iirc have better types.

I'm also of the opinion that Number() is ok and is widely documented and recommended. So I probably won't switch our teaching to parseInt / parseFloat.

@bradzacher
Copy link
Member
8000

I'm also of the opinion that Number() is ok and is widely documented and recommended. So I probably won't switch our teaching to parseInt / parseFloat

I mean the type definition of Number.parseInt/Float is strict and only accepts string.

It seems weird that you want a lint rule to ensure correct usage of Number when you could just ban it and instead use parseInt/Float and have correctness enforced by TS at typecheck time.

@bradzacher
Copy link
Member

Too bad that typescript-eslint won't add a no-base-to-string-symmetrical rule for Number()

There isn't a symmetry here though.

toString is a method that's declared on every type and value and it is implicitly called in countless scenarios.

OTOH there is no toNumber method that is implicitly called - the only way to convert a value to a number is Number(), parseInt/Float, to use it in a binary operation with a math operator, or to use a unary +/-. Of those cases parseInt/Float is strict and only accepts string, the binary case is handled by TS itself.
The unary case is unhandled but is so so so niche that it's not worth thinking about, and the Number case can be replaced with parseInt/Float for safety.

It just doesn't seem broadly valuable to build a rule for this, IMO?

@kirkwaiblinger
Copy link
Member

@karlhorky FYI if you do end up doing syntactic bans of these functions I would suggest allowing the argument to be a satisfies expression, that way you can demand a good type at each of the usage sites. Just a thought.

@karlhorky
Copy link
Author
karlhorky commented Feb 28, 2025

It just doesn't seem broadly valuable to build a rule for this, IMO?

I guess I disagree on that:

  1. Number() is widely documented and used - also, it has nice symmetry with String() and Boolean()
  2. There is no rule guiding away from Number() and towards parseInt(), parseFloat()
  3. Even if there is other syntax that allows for safer usage, I think it's good to also make Number(), String(), etc. safe, for the existing usage

I think these types of "safety" rules (including also the rules for string interpolation) can be argued against with buried "best practices" comments on issues, but unless these best practices are exposed to users, users will keep getting bitten by this. My gut feel says most users won't know that it's possible to guard against this, so won't ask / search for this.

Developing a rule would expose these best practices and offer high value right away to a wide range of codebases.

@Josh-Cena
Copy link
Member

A few points:

  • I recommend Number() over other parsing functions, because its parsing rules are what you expect: exactly how you parse number literals. Both parseInt and parseFloat "do their own thing" and it's not always easy to remember what it accepts or does not.
  • There in fact does exist a generic number coercion method: valueOf(). A rule that bans implicitly calling the base valueOf() method would also be desirable. Alternatively, you could well claim that Number() indeed calls toString(): because valueOf() returns an object, it gets ignored, so the string coercion result is parsed to a number instead, which happens to be invalid. This is documented in https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number#number_coercion

So now I think about it, I think we should have a no-base-to-string equivalent for number coercion that catches +x and Number().

@JoshuaKGoldberg
Copy link
Member

What is the resolution here? Just having a no-base-to-number-esque rule?

@bradzacher
Copy link
Member

I'm a -1 because this is my 2c for codebases that care about it:

{
  "rules": {
    "no-restricted-syntax": [
      "error",
      {
        "selector": ":matches(CallExpression, NewExpression) > Identifier[name = 'Number'].callee",
        "message": "Don't use the number constructor, use parseInt/parseFloat"
      },
      {
        "selector": "UnaryExpression[operator = '+']",
        "message": "Don't use implicit number coercion, use parseInt/parseFloat"
      }
    ]
  }
}

playground

But I ofc won't block anyone progressing on this.

@JoshuaKGoldberg
Copy link
Member

Coming back to this, it's a really interesting discussion and we see the potential use. But Number()/+/etc. practices are much less common (and less defined) than String()/etc. It's hard to justify spending the space+time to put the rule in core.

Marking as evaluating community engagement to see if other folks want to chime in. If anybody can post links to other discussions asking for this, real-world bugs/similar this would have been helpful in, or other supporting evidence, please do!

@JoshuaKGoldberg JoshuaKGoldberg added evaluating community engagement we're looking for community engagement on this issue to show that this problem is widely important and removed triage Waiting for team members to take a look labels Apr 7, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement: new plugin rule New rule request for eslint-plugin evaluating community engagement we're looking for community engagement on this issue to show that this problem is widely important package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin
Projects
None yet
Development

No branches or pull requests

5 participants
0