-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Unsafe type-incompatible assignments should not be allowed #14150
Comments
Duplicate of #13347 |
@mhegazy This issue overlaps with #13347, certainly, but it's not a duplicate. There are more issues here than simply type A = { val: string; };
type B = { val: string | number; };
const a: A = { val: "string" };
const b: B = a;
b.val = 3;
console.log(a.val.toUpperCase()); // throws |
A literal is in the domain of string, similarly a string is in the domain or string or null. These work as intended. |
I'm sorry I'm not making myself clear, but that's not the point. The string value type is not the problem. The reference type containing those value types is the problem. Again, I'll point out that Flow does not allow these assignments, because they are unsafe. In my last example, type |
Thanks for the explanation. TypeScript originally did not have When We have talked about |
I understand, but ignore const a: { val: string } = { val: "str" };
const b: { val: string | number } = a; The second assignment should not be allowed. If |
This is unsafe if |
As I said, please ignore The problem is fundamentally about allowing incompatible type assignments. This is a problem regardless of whether I'm not suggesting that the value should be immutable, simply that allowing it to be assigned a Here's my example again, annotated further to explain the problem in more depth: const a: { val: string } = { val: "str" };
// There is now one reference to the object.
// The object's type is `{ val: string }`.
const b: { val: string | number } = a;
// There are now two references to the **same** object. We did not copy the
// object, we simply added another reference.
//
// However, our two references treat the object as different, incompatible
// types. Flow does not have `readonly`, but Flow does not allow the previous
// assignment.
b.val = "a different string";
// This is OK. The object being mutable is fine, so long as the types match.
// This does not break `a`. `a` expects `val` to be a `string`, and it is.
console.log(a.val.toUpperCase());
// Yay, `a.val` is the type we expected, so this logs correctly.
b.val = 3;
// Since `b` references the same object as `a`, this breaks `a`. The inner
// `val` is now a number, and not the expected `string`.
console.log(a.val.toUpperCase());
// This throws an Error, because `a.val` is no longer a `string` like the type
// specifies it should be. |
@mhegazy I've updated the original issue description to remove all references to |
const a: { val: string } = { val: "str" };
const b: { val: string | number } = a; in this example. the assignment is safe if you never write to The problem is, as you noted, if you were to write to b.val = 2;
a.val.indexOf(""); // Error Since TS did not have a way to designate a property as Again since we did not have a way to track accuratelly what is If we have a mode (again const c: { readonly val: string | number } = a; |
Right, but even if we supported
Yes, but since we don't have a mechanism in the language for preventing that, my contention is that the assignment itself should be disallowed (as it is in Flow). If I want to ignore the error, I should have to assign it with a type assertion |
That is what i meant by "we opted to error on the side of usability.". and that is what i was proposing with |
I should have clarified that I expected such a change would need to live behind a flag. I apologize for leaving that out. However, I maintain that this is a separate issue from So,
My concern is that |
Would interface A { kind: "A"; foo(): void; }
interface B { kind: "B"; bar(): void; }
function f(x: A | B): void {
switch (x.kind) {
case "A": x.foo(); break;
case "B": x.bar(); break;
}
}
function setKindToB(x: A | B): void {
x.kind = "B"; // clearly unsafe
}
const a: A = { kind: "A", foo() {} };
setKindToB(a); // uh oh...
f(a); // crash: x.bar is not a function |
I would say so. the call |
For clarity, do we agree that there could be a separate Whether or not such a flag would get prioritized and added is a separate issue, but I'd like to confirm that we're on the same page regarding the distinctness of this issue as compared to #13347. |
I agree that type AB = { prop: "A" | "B" };
type BC = { prop: "B" | "C" };
function f(x: AB | BC) {
x.prop = "B"; // safe, should compile
x.prop = "C"; // unsafe, should not compile
} Maybe this can be solved by giving properties distinct "readable" and "writable" types. In this example, |
I would say it should be |
Fair enough. 😄 Would it be reasonable to remove the "Duplicate" label and judge this on its own as a suggestion to add a |
Hi @RyanCavanaugh, I noticed that this suggestion was discussed in #14490. I was hoping you could go into a bit more detail about this comment:
I'm curious if that refers to implementing support within TypeScript? Or if it refers to using the flag were it to be implemented? The projects of course have different approaches, but I feel it's worth mentioning that Flow made object property types invariant by default a number of months ago. |
Sorry, forgot to flush the notes to this issue.
This. Anyone who wants to use this flag would have to go through every definition file they reach and add property variance annotations. They may even have to split up types into separate "input" and "output" types. A similar exercise is to try to add I think the Flow type system is heading in a very complex direction by enforcing property variance before implementing array covariance - it's going to be a major headache either way at some point in their near future. For example, this program does not have an error in Flow (flowtype.org/try at v0.41), but has the same problem as the aliased-object situation in TypeScript: function addANumber(arr: Array<string | number>): void {
arr.push(10);
}
let x = ['hello'];
addANumber(x);
let j = x.pop();
j.substr(2); // No error, but fails at runtime |
Thanks for following up, and for the additional details. I appreciate it. I just have a few comments.
That's only true where the property types of the objects varied. If you're dealing with a fairly regular set of interfaces, I don't believe this would change much at all.
I'd agree with that, but I think the tradeoff would be worthwhile. We found this to mostly be true with If there's further discussion to be had here, I'd love to contribute however I can. In either case, thanks again for the notes and for considering the suggestion. |
I'm interested in adding strict variance to TypeScript. I started working on this a few weeks ago and have it work for a few cases, see kevinbarabash#4. In that PR the flag is called A couple of notes:
I think providing a way to enable/disable various strict flags would be useful for this. Beyond just having to update an entire program, I think this particular feature will probably also cause issue when using library definitions that haven't been upgraded yet. In the PR I linked to above I was experimenting with requiring pragmas in .d.ts files in order for the feature to work with the functions from that library. For the feature to actually work the following must be true:
I think some variation of this solution make work for rolling out new strict flags that break existing code and/or library definitions. I think having pragmas to selectively enable/disable the strict variance within source code well as library code would be useful too. I wonder if people would find these kinds of pragmas useful in adopting existing strict flags. |
Hello everyone, Just thought I'd drop this here, It's a prototype typescript-eslint rule that attempts to make all mutable properties invariant. I've tested it on some projects at work and it's not 100% accurate but it does the job for the most part. While waiting for full typescript support, this might be a good interim solution. There's no npm package yet because this is still a prototype. You can make it a custom rule for your projects, though. |
Hi, what's the state of this discussion? it seems it stalled for some time... I know introducing new keywords and syntax changes is usually disliked and discouraged, and for good reasons, but maybe we could improve a little bit the situation (without having to rely on flags that change the code semantics) by introducing a "sister" keyword for It's true that introducing this keyword would not fix old bugs in old codebases, but would allow us to do a smoother transition into safer (and more explicit) code for people who need it or care about. Just as an example: function messUpTheArray(mutable arr: (string | number)[]): void {
arr.push(3);
}
const strings: string[] = ['foo', 'bar'];
// This wouldn't be allowed because of the `mutable` qualifier in the function signature
messUpTheArray(strings); const a: { val: string } = { val: "str" };
// Not allowed, as we know in advance that we'll mutate the object referenced by b
const b: mutable { val: string | number } = a;
// Not allowed, as we know in advance that we'll mutate the object referenced by c.val
const c: { mutable val: string | number } = a
// Allowed
const d: { val: string, mutable foo?: number } = a Of course this has its own problems, as it does not seem to be very useful beyond function signatures and reference duplications (given that almost everything is mutable by default in JS & TS). P.S.: I see that there was a discussion about introducing |
TypeScript Version: 2.1.6
Code
Expected behavior:
The sample should not compile, as it's not safe. Type casts/assertions should be required to override type incompatibility errors here. (Or, of course, cloning the object itself
const nullable: StringOrNull = { ...str };
).For comparison, Flow does not allow the sample code.
Actual behavior:
The sample compiles and further accesses of
str.val
will likely result in exceptions.EDIT: Removed all references to
readonly
, as discussion of that modifier is overshadowing the issue.The text was updated successfully, but these errors were encountered: