Skip to content

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

Closed

Description

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), Anders Hejlsberg (@ahejlsberg) was asking for further practical examples that would motivate stricter typing of modifying operators, which this report provides imho.
Bruce Pascoe (@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.

Activity

  1. fwienber commented on Apr 27, 2022

    @fwienber
    Author

    Martin Johns (@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.

  2. fwienber commented on Apr 27, 2022

    @fwienber
    Author

    Martin Johns (@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).

  3. fwienber commented on Apr 27, 2022

    @fwienber
    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";
  4. andrewbranch commented on Apr 27, 2022

    @andrewbranch
    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.

  5. fwienber commented on Apr 27, 2022

    @fwienber
    Author

    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?

  6. andrewbranch commented on Apr 27, 2022

    @andrewbranch
    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.

  7. MartinJohns commented on Apr 27, 2022

    @MartinJohns
    Contributor

    Martin Johns (@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)).

  8. fwienber commented on Apr 27, 2022

    @fwienber
    Author

    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.

  9. fwienber commented on Apr 27, 2022

    @fwienber
    Author

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

  10. MartinJohns commented on Apr 27, 2022

    @MartinJohns
    Contributor

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

  11. fwienber commented on Apr 27, 2022

    @fwienber
    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.

  12. fwienber commented on Apr 27, 2022

    @fwienber
    Author

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

  13. MartinJohns commented on Apr 27, 2022

    @MartinJohns
    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".

  14. fatcerberus commented on Apr 27, 2022

    @fatcerberus

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

  15. fwienber commented on Apr 28, 2022

    @fwienber
    Author

    Please continue here: #47027 (comment)

  16. fwienber commented on May 5, 2022

    @fwienber
    Author

    Andrew Branch (@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!

  17. locked as resolved and limited conversation to collaborators on Oct 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    DuplicateAn existing issue was already created

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions