Skip to content

Modifying operators must only be allowed for type number (and += for string) #48857

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

Closed
fwienber opened this issue Apr 27, 2022 · 16 comments
Closed
Labels
Duplicate An existing issue was already created

Comments

@fwienber
Copy link

fwienber commented Apr 27, 2022

Bug Report

In this report, I'd like to motivate that the type checker should not allow applying modifying operators to custom sub-types at all.
For reference and all kinds of unexpected behavior because this is currently allowed, see #14745, #47027, #30783 and an attempt to fix (part of) this behavior, #28344.
In #14745 (comment), @ahejlsberg was asking for further practical examples that would motivate stricter typing of modifying operators, which this report provides imho.
@fatcerberus #47027 (comment) agrees that any usage of := on something that is not of type string is "code smell".

🔎 Search Terms

  • increment operator
  • decrement operator
  • assignment operator
  • modifying assignment

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about the search terms

⏯ Playground Link

Playground link 1: Integer Range Example

Playground link 2: Unsigned Integer Example

💻 Code

Example 1: Integer Range

type ZeroToThree = 0 | 1 | 2 | 3;
// a function that coerces any given number to the integer range 0..3:
const ZeroToThree = (value: number): ZeroToThree => (value >>> 0) % 4 as ZeroToThree;

let a: ZeroToThree = 3;
const b: ZeroToThree = a + 1; // error, good, because + always results in a 'number', not necessarily in range of ZeroToThree
const c: ZeroToThree = ZeroToThree(a + 1); // type-safe _and_ run-time-safe!
const d: ZeroToThree = a - 1 as ZeroToThree; // if you know what you're doing and want to avoid run-time overhead, use 'as'
a += 1; // no error, bad, because a is now out of range (4)
++a; // no error, too, also bad
console.log(a); // a is now 5 :-(

Example 2: Unsigned Integer

type uint = number & {__uint__: never};
const uint = (value: number): uint => (value >>> 0) as uint;

let a = uint(0);
const b: uint = a - 1; // type error; you are forced to use the coercion function!
const c: uint = uint(a - 1); // no error when using coercion function (leads to MAX_UINT = 4294967295)
let d = uint(1); // type inference works: hover over 'd' shows 'let d: uint'
d -= 1; // here, TypeScript correctly complains "Type 'number' is not assignable to type 'uint'." 
--d; // no type error, although it behaves the same as the line above
d--; // no type error, too: 'd' is an uint, but becomes -2 :-(
const e: uint = --d; // again, a correct type error, so the _result_ of --d is correctly typed as 'number'

🙁 Actual behavior

Modifying operators like ++, --, +=, -=, *=, /= etc. are allowed for types that specialize number (and for +=, string), although the result does not necessarily adhere to the constraints given by the sub-type.

🙂 Expected behavior

Modifying operators are only allowed for the type number (plus string for +=) itself.
When using a sub-type of number, like a literal type, a union of literal types (like in the "Integer Range" example), or an intersection of number with any other type (like in the "Unsigned Integer" example), an explicit type conversion or type assertion is needed.

Like for other changes that lead to stricter typing, this behavior could be activated through a compiler option.

Rationale:

The built-in numeric operators in ECMAScript, +, -, *, /, % are all defined for number arguments (except for overloaded '+') and always result in a number (maybe NaN, which is also of type number). At compile-time, it is in most cases impossible to check that a sub-type of number is maintained by any of these operators. Thus, the result is only safely assignable to number (or a more generic type, essentially only any), not to a more specific type.

Any modifying operator used with a sub-type of number (and += used with a sub-type of string) would then be a type error. To resolve this error is not an unacceptable burden for the developer, because modifying operators are just a shorthand for an assignment. When expanding to an assignment, the right-hand-side can easily be complemented by a type assertion (if you are sure the computation result stays within the constraints of the sub-type for semantic reasons the type checker cannot deduce) or a conversion function that ensures the constraints at run-time (see both examples). It could be discussed whether there should be special syntax to add a type assertion to modifying operators that refer to the right-hand side only (note that ++foo as uint is only an assertion for the expression result, not for the right-hand side foo + 1 of the implicit assignment), but I don't think this is necessary.

The examples show typical use cases, where you want to enforce a restricted set of number values, which is implemented through a coercion function at run-time. You want the compiler to check that you never forget to apply the coercion function to a number before assuming the result is one of the restricted values. This works very well in TypeScript, but applying modifying operators currently disables the check in a way only a type assertion should.

The same argument can be made for string and its specializations, like unions of string literals or string template types. For string, the only meaningful modifying operator is +=. I tried to add this special case in parenthesis throughout this report.

@fwienber
Copy link
Author

fwienber commented Apr 27, 2022

@MartinJohns, what is the confusion here? Can you clarify, so I could try to respond?
Already saw your posts in the other issues, so I am aware that you are interested in this topic, too.

@fwienber
Copy link
Author

@MartinJohns Your issue #47027 focuses on += on string, while mine focuses on all modifying operators on number and mentions the string / += case only as a side-note.

If this did not become apparent, I totally agree with you that for example

let foo: `${string}.png` = "foobar.png";
foo += "/";

should result in a type error.
The += operator should only be allowed on variables or properties of the exact type number or string, not on any more specific type (sub-type).

@fwienber
Copy link
Author

Maybe another example, where one could be surprised by the type error:

type SomeAs = "a" | "aa" | "aaa";
let aa: SomeAs = "a";
aa += "a";

Although "aa" would still meet the constraints of SomeAs, the compiler cannot check this for the general case (it would have to do all kinds of computations at compile-time, and once you introduce loops or conditions, this becomes impossible), so it should better reject it and enforce an explicit type assertion rather than be "optimistic" about such operations.
As mentioned before, this would be consistent with the type error you get when you try

aa = aa + "a";

@andrewbranch andrewbranch added the Duplicate An existing issue was already created label Apr 27, 2022
@andrewbranch
Copy link
Member

Thanks for the clear and thorough report; however, the original issues are not locked so if you want to continue the discussion, you can do so in those issues. Opening new issues to discuss old ones just creates more triage work and makes it harder for future readers to follow the conversation.

@fwienber
Copy link
Author

fwienber commented Apr 27, 2022

Some of these issues are already closed as won't fix, and the argument I am trying to make is a bit different, so none of them fit 100%.
I think the root issue is not about literal types or only strings, as all the former issues I found (#14745, #28344, #30783, #47027) seemed to assume. I hope my "Unsigned Integers" example shows this aspect (no literal type, string or union type anywhere). I would like to propose the new viewpoint that all modifying operators may only be used on variables and properties of exactly the type number (and += additionally on exactly the type string). Which existing issue do you think would fit that approach?
In other words, if you label it "Duplicate", of which issue do you think it is a duplicate?

@andrewbranch
Copy link
Member

It’s a duplicate of #14745 and #47027. The uint example is indeed a new and interesting point but it doesn’t rise to the level of needing a whole new issue.

@MartinJohns
Copy link
Contributor

@MartinJohns, what is the confusion here? Can you clarify, so I could try to respond?

I'm confused to why you open a new issue when you already found an existing open (#47027) which has the label "In Discussion".

Your issue #47027 focuses on += on string, while mine focuses on all modifying operators on number and mentions the string / += case only as a side-note.

The focus on string is irrelevant IMO. It's fairly clear that this applies to any literal type and any assignment operator. Ryan even linked another issue where the focus is numbers (#47027 (comment)).

@fwienber
Copy link
Author

fwienber commented Apr 27, 2022

Okay, I found this discussion rather fragmented and after realizing that #14745 had been closed as "won't fix", I tried to consolidate it in a new issue with a concrete suggestion that is supposed to tackle all special cases.
But if you prefer, I would be happy to close this issue and add the "uint" example to #47027. Would it be an option to clarify the more general claim of that issue by adapting the subject? As explained above, I think "literal types" are not the only source of the problem.

@fwienber
Copy link
Author

Well, maybe I tried to "solve fragmentation by adding another fragment", like in https://xkcd.com/927/

@MartinJohns
Copy link
Contributor

I've changed the title and mentioned that it applies to number as well, but I didn't add the mention of uint.

@fwienber
Copy link
Author

Would it be an option to clarify the more general claim of that issue #47027 by adapting the subject? As explained above, I think "literal types" are not the only source of the problem.

Thank you for already changing the title!

I still think that, as explained above, "literal types" are not the only source of the problem, so I'd suggest "constrained types" or even "subtypes of number / string" or something like that.

@fwienber
Copy link
Author

And I'll add the "uint" example there, but maybe not today (as it is already 8 pm in my time-zone).

@MartinJohns
Copy link
Contributor

Literal types can only be string, number and bigint (and well, true and false). No other. It's literally the name for these "subtypes of number / string".

@fatcerberus
Copy link

@MartinJohns He also intends for the rule to apply to e.g. number & { __brand: never } style types too.

@fwienber
Copy link
Author

Please continue here: #47027 (comment)

@fwienber
Copy link
Author

fwienber commented May 5, 2022

@andrewbranch After following your advice to close this issue and add my examples and suggestions to #47027, I would be delighted to get some reaction there!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

4 participants