-
Notifications
You must be signed in to change notification settings - Fork 12.7k
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
Narrowing lost in polymorphic function application #27259
Comments
A smaller repro: type XY = 'x' | 'y';
const x: XY = 'x';
let x2 = x; // Has type string We definitely shouldn't be widening the type of |
@weswigham @RyanCavanaugh We should look at this one right away. |
Timing seems to indicate this is an effect of #27042. |
I can point exactly to what causes this, because (at least for booleans, which were the motivator) it was intentional. In type XY = 'x' | 'y';
const x: XY = 'x';
let x2 = x; // Has type string
const x: boolean = true;
let x2 = x; // Has type boolean Without this rule, |
@weswigham See my comment here. |
Repeating my comment in #27042, the intuitive rule was always that once you add a type annotation to let, const or var declaration, values read from that variable are not fresh. But now they are unless the annotated type is a unit type. const x1: 'x' = 'x';
let z1 = x1; // 'x'
const x2: 'x' | 'y' = 'x';
let z2 = x2; // Was 'x', now string I find it hard to construct a rationale for that. |
Hm, but the annotation actually wasn't why that was the case, strictly speaking. It was simply because the types we were narrowing by weren't fresh. If by chance you narrowed by a union of fresh types (because, eg, the union type is built up from conditional expressions) it would behave exactly like this already, even if it were annotated! |
The reason I added the change was for consistency, because a union of fresh types like that is built for booleans all the time (while it's more rare for other literals) and consistiency between the declared and constructed forms of the types seemed desirable. |
As I said above, the core intuition was always that once a type annotation was present in the path that a value traveled, the value would loose its freshness. That's broken now in a way that I don't think I can explain. I think we need to stick with the old rules. If they don't work for fresh booleans then maybe we don't need fresh booleans. |
It's more like the old rules are very unintuitive for booleans which previously widened everywhere.
We could choose preserve that intuition with a different change (ie, by actually explicitly having a rule governing it) - by explicitly stripping the freshness measured from an initializer of a statement with a declared type. |
Ideally, when narrowing occurs, the type it narrowed from would be saved. Then, if it has to widen, it widens to the saved type.
I think disregarding ": b" when c is assigned to a, is rather confusing. In fact, if ": b" is completely ignored, it shouldn't even be allowed / it should be warned of by the linter, as writing code that becomes a noop is rarely expected behavior. Even in the original functionality d would be of type "c" - not what the user likely intended. Most think of constants by their type and value, and dont think of a constant's type being the value itself (As in C/C++ const int a = 5; where a is logically an integer. 5 can be used by the compiler and linting. auto b = a; still keeps b an integer). I haven't contributed to this repo but I'm sure my suggestion is a rather large overhaul anyway; at least it's food for thought.
I think this logic is a lot simpler than having literals being types, and causing an overload for what 'type' represents. On the same token,
works perfectly (Which is what all this freshness was designed to achieve in the first place, as far as I understand - no guarantees Im understanding this well however). |
@ahejlsberg here's my summing of the situation. It feels like we're at a local maximum at least, but there's one example where it seems there's some disagreement about the desired behavior: function f1() {
let b = true;
let obj = { b };
// Desired: OK
// 3.0: OK
// 3.1 as-is: OK
// 3.1 minus widening propagation: error
obj.b = false;
}
function f2() {
type Element = (string | false);
type ElementOrArray = Element | Element[];
let el: Element = null as any;
let arr: Element[] = null as any;
let elOrA: ElementOrArray = null as any;
// Desired/actual: All OK
let a1: ElementOrArray = el;
let a2: ElementOrArray = arr;
let a3: ElementOrArray = [el];
let a4: ElementOrArray = Array.isArray(elOrA) ? elOrA : [elOrA];
// Desired: OK
// 3.0: Error
// 3.1: OK
let a5: ElementOrArray = [...Array.isArray(elOrA) ? elOrA : [elOrA]];
}
function f3() {
type XY = 'x' | 'y';
const x: XY = 'x';
let x2 = x;
// Desired: OK (up for debate?)
// 3.0: Error
// 3.1 as-is: OK
x2 = 'y';
// Desired/actual: All OK
let x3: XY = x;
x3 = 'y';
}
function f4() {
const x: boolean = true;
let x1 = x;
// Desired: OK
// 3.0: OK
// 3.1: OK
// 3.1 minus widening propagation: error
x1 = false;
}
function f5() {
type XY = 'x' | 'y';
let arr: XY[] = ['x'];
arr = ['y'];
// Desired: OK
// Error in all extant branches
arr = [...['y']];
} |
The widening approach breaks all efforts put into excluding union cases and prevent relying on the type checker for precise exhaustive cases checking. |
thanks @weswigham ! |
will this fix a huge issue in typing one of our tools?
|
TypeScript Version: 3.1.0-dev.20180921 (next)
[email protected] <- works as expected.
[email protected] <- problem started to appear at that very version.
Search Terms:
generic inference regression
Expected behavior:
Expected union type to be preserved
Actual behavior:
Union type is expanded to
string
Playground Link:
This Playground Link demonstrate the CORRECT Behaviour (because previous typescript version is used in the playground)
http://www.typescriptlang.org/play/#src=type%20XY%20%3D%20'x'%20%7C%20'y'%0D%0Aconst%20x%3A%20XY%20%3D%20'x'%0D%0Ainterface%20Opt%3CT%3E%20%7B%0D%0A%20%20v%3A%20T%0D%0A%7D%0D%0Aconst%20foo%20%3D%20%3CT%3E(v%3A%20T)%3A%20Opt%3CT%3E%20%3D%3E%20(%7B%20v%20%7D)%0D%0Aconst%20foox%20%3D%20foo(x)%20%2F%2F%20Opt%3Cstring%3E%20instead%20of%20Opt%3CXY%3E%20(Regression)%0D%0A%0D%0Aconst%20bar%20%3D%20%3CT%3E(v%3A%20T)%3A%20Opt%3Ctypeof%20v%3E%20%3D%3E%20(%7B%20v%20%7D)%0D%0Aconst%20barx%20%3D%20bar(x)%20%2F%2F%20Opt%3Cstring%3E%20instead%20of%20Opt%3CXY%3E%20(Regression)%0D%0A%0D%0Aconst%20baz%20%3D%20%3CT%3E(v%3A%20T)%3A%20T%20%3D%3E%20v%0D%0Aconst%20bazx%20%3D%20baz(x)%20%2F%2F%20'x'%20(OK)%0D%0A
Related Issues:
I think this bug is very recent; I've not found anything relevant.
The text was updated successfully, but these errors were encountered: