-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Assignment operators allow assigning invalid values to literal types #47027
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
Comments
Here's yet another related example of odd string literal type behavior; let example_one = 'first' as const;
example_one += ' append';
//> No error?! From what I understand the act of type name = 'second';
let example_two: name = 'second';
example_two += ' append'; Because when I inspect the TypeScript type of
... But inspecting TypeScript types after
|
Much confusion here shown in initial report. I'm sure there are more specific doc mentions, but regarding objects this mentions:
That is, something like this is not protected and produces no errors:
Same doc mentions later that a strange construct like this will protect against property modification:
But there it is the entire object literal that is being declared constant (or actually the properties 'readonly') In the second comment I was struck by:
Consider the difference between that and
There is no difference. Neither generates an error. You have marked the literal string 'first' as constant. And every string literal is constant. You have said nothing about the variable
fails as expected because now the variable is marked as constant. I'm afraid I see nothing surprising here (except the Please consult the docs again. Each time more things make sense. |
@tshinnic You missed the point of the issue by a staggering degree. There's absolutely no confusion present, and I'm very aware that the object is mutable. The point is about the You also don't seem to know what const contexts (aka
Or to make a more elaborate demonstration with objects using your example:
|
Oh good, a teachable moment awaits. |
Personally I'd expect to have to leverage something like type appendable<P, T> = P & T;
interface Data_One {
value: appendable<'prefix', string>;
}
const data_one: Data_One = { value: 'prefix' };
data_one.value += ' append'; ... And for TypeScript to throw an error for previously posted examples. But at this time even appending types such as numbers are allowed, eg. type Data_Two = { value: 'prefix' }
const data_two: Data_Two = { value: 'prefix' };
data_two.value += 1; |
@S0AndS0 That would be
|
See also #14745 (comment) This is a bit of a weird spot. Mutation of a value of a single literal type is clearly wrong, but also unlikely to be something you do accidently, so it's a low-value error despite high confidence. However, mutation of a value of a union of literal types is likely to be correct in a way that we can't statically verify, so it's a false positive. The natural way to split that would be to say that mutation of non-union literals is disallowed but mutation of union literals is "presumed OK", but this is a subtyping violation because an operation that's allowed on This code smells really bad, though, and it'd be nice to find a more reasonable solution. OT: To whomever it may concern, please take my personal ensurances that OP understands what they are talking about. |
I just knew I have seen this issue before, but just couldn't find it. |
I would personally be very suspicious of any code that switched between members of a finite set of known string literals by way of mutation rather than direct assignment and wouldnβt mind the (hypothetical) error that disallowed it. My opinion of this pattern for number literals is admittedly more fuzzy though, given that TS doesnβt have range types. |
I would like to contribute a few more examples that illustrate that assignment operators and increment/decrement operators (that is, all modifying operators) are currently typed inconsistently in TypeScript. I also put them in a TypeScript Playground. Example 1: Assignment Operators Bypass Range (Union) Type CheckThe first example shows a typical way to define and check an integer range type, but the attempt to enforce calling the coercion function is undermined by using assignment operators. 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 z: ZeroToThree = 3;
const y: ZeroToThree = z + 1; // error, good, because + always results in a 'number', not necessarily in range of ZeroToThree
const x: ZeroToThree = ZeroToThree(z + 1); // type-safe _and_ run-time-safe!
const w: ZeroToThree = z - 1 as ZeroToThree; // if you know what you're doing and want to avoid run-time overhead, use 'as'
z += 1; // no error, bad, because a is now out of range (4)
z *= 2; // any numeric operator can be used to bypass the range check
console.log(z); // z is now 8 :-( Example 2: Increment/Decrement Operators Bypass Type CheckThe second example is based on the pattern to implement "tagged" number types, here an "unsigned integer" type. type uint = number & {__uint__: never};
// a function that coerces any given number into an unsigned 32 bit integer:
const uint = (value: number): uint => (value >>> 0) as uint;
let a = uint(0);
const b: uint = a - 1; // type error; good: you are forced to use the coercion function!
const c: uint = uint(a - 1); // good: no error when using coercion function (leads to MAX_UINT = 4294967295)
let d = uint(1); // good: type inference works: hover over 'd' shows 'const d: uint'
d -= 1; // here, TypeScript correctly complains "Type 'number' is not assignable to type 'uint'."
--d; // bad: no type error, although it behaves the same as the line above
d--; // bad: no type error, too: 'd' is an uint, but becomes -2 :-(
const e: uint = --d; // good: type error, the _result_ of --d is correctly typed as 'number'
const f: uint = d++; // bad: type error, although the result of post-increment is the old value, declared as 'uint' What is really strange is that Example 3: No Type Narrowing When Using
|
Consequences: The suggested stricter typing would of course lead to most usages of assignment operators, except on for (let i = 0 as uint; i < 10; ++i) { ... }
for (let i = 0 as uint; i < 10; i = uint(i + 1)) { ... } Because you know that for (let i = 0 as uint; i < 10; i = i + 1 as uint) { ... } Since changing your code like that can result in quite some work, like for other breaking changes in TypeScript, new compiler flags should be introduced to (de)activate the stricter checks, something like |
A few corrections of my suggested new typing rules:
This is not precise, as in ECMAScript, the increment operator does not use the overloaded var foo = "a";
++foo; // the expression result as well as the new value of foo is NaN, not "a1"
var bar = "b";
bar += 1; // the expression result as well as the new value of bar is "b1" So in this case,
This is also not correct, as in ECMAScript, the coercion to var foo = "a";
foo++;
// foo is NaN, but the expression result is also NaN, not the old value "a"! So the fix would be:
But since additionally, TypeScript does not allow usage of arithmetic operators on anything non-numeric, the two rules about increment/decrement operators can be corrected and combined to
Let's have a look at the special case that the operand has type (value: any) => {
const a: number | bigint = ++value;
const b: number = ++value;
const c: bigint = ++value;
} In the example, TypeScript currently shows a type error only for the last line, because it infers the type of |
After adding several examples for inconsistent / undesirable behavior of assignment/increment/decrement operators and suggesting an approach for a possible solution, I'd be glad to hear some feedback by the TypeScript community and/or maintainers, if you think this is valid and viable... |
Btw., the "uint" example was inspired by https://spin.atomicobject.com/2018/11/05/using-an-int-type-in-typescript/ . |
After learning details about ECMAScript Update Expressions (prefix / postfix increment / decrement operator), it seems to me they are quite a different kind of beast, so I would like to pull out the corresponding TypeScript issues I found into a separate issue. |
If this is so, then why does let x: 0 | 1 | 2 = 0;
x = x + 1; result in a type error, namely I strongly suggest assignment operators and update expression should result in the same type errors as semantically equivalent expressions.
I don't want any "type math" or added complexity, I just want consistency, and for that, have the option to handle assignment operators and update expressions in a strict way, i.e. be treated just like semantically equivalent expressions. |
Here is another TypeScript playground with consolidated, concise examples of everything that goes wrong with assignment operators and update expressions. |
Is there no interest in this topic, or just no attention because this issue is not new? Or is it just that at the moment, the TypeScript team is under release stress (4.7)? My first attempt to "revive" this issue is now 18 days old, and there has not been a single reaction by anyone since then. |
I think with all the examples of inconsistent behavior provided in this issue, it at least deserves a "Bug" label. |
Since the label "Suggestion" seems to cause this issue to stay under the radar, I extracted one of the most obvious, concrete bugs into a new "Bug" issue: #49558 |
Bug means it's supposed to work a specific way, but it doesn't. This is not the case here. This behaviour was never supposed to work this way, it was never implemented. It unsound behaviour, yes, but not a bug. This issue doesn't get a lot of traction because it's a corner-case. Hardly anyone ever stumbles upon this problem. The team has limited resources that are better spent on more common features and issues. And while this issue is not nice, having a sound type system is explicitly a non-goal. |
That's why instead of insisting on labelling this issue a "Bug", I created a new issue with one of the examples where the type system is not only non-sound, but downright contradictory and clearly wrong/outdated with respect to ECMAScript semantics. |
Bug Report
π Search Terms
addition assignment string literal
append string literal
π Version & Regression Information
β― Playground Link
Playground link with relevant code
π» Code
The same applies to any literal type (e.g.
number
). See #48857 for more examples.π Actual behavior
Appending a random string to the property typed as the literal
"literal"
is possible.π Expected behavior
I would expect an error, saying that the the type
"literalfoo"
is not assignable to"literal"
.Ping @S0AndS0
The text was updated successfully, but these errors were encountered: