You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
typeFAILURE="FAILURE";constFAILURE="FAILURE";typeResult<T>=T|FAILURE;functiondoWork<T>(): Result<T>{returnFAILURE;}functionisSuccess<T>(result: Result<T>): result is T{return!isFailure(result);}functionisFailure<T>(result: Result<T>): result is FAILURE{returnresult===FAILURE;}functionincrement(x: number): number{returnx+1;}letresult=doWork<number>();if(isSuccess(result)){increment(result);// ~~~~~~// Argument of type 'string | number' is not assignable to parameter of type 'number'.// Type 'string' is not assignable to type 'number'.}
Expected behavior:
result (before the narrowing) should be "FAILURE" | number
It seems like the change is somewhat intentional – that let is getting the "base primitive types" now. However, since the function (doWork, and also isSuccess and isFailure have explicit annotations), I suspect it is possible to keep this pattern working without breaking the other goals?
the change is intentional. see #10676 for more details. I do not think this is a pattern we explicitly thought about; so we need to go back and talk about this.
a work around would be an explicit type annotation to result: let result: Result<number>
Mohamed Hegazy (@mhegazy) keep me posted! 👍 I did try to read #10676, and there seems to be some thoughts/effort in making functions with explicit return type annotation work, so I am crossing my fingers :P
The issue here is that when we infer the type number | "FAILURE" for a mutable location, we can't tell that "FAILURE" is a special type that you don't want widened to string. Indeed, had the type been 1 | "FAILURE" you probably would have wanted the 1 to be widened to number.
One possible solution is to change the first two lines to:
This turns the FAILURE type into an intersection that won't be widened, but otherwise is similar to the literal type "FAILURE". But I think an even better change would be to use an object as the sentinel failure value. That way Result<T> would also work when T is a string.
I agree the last solution would work and arguably better, but there are still a few things that doesn't feel right to me:
When there is an explicitly typed return value, it feels wrong to infer the type on the let, instead of just taking the returned type verbatim. IMO, the explicit return type is a pretty strong signal that you know what you are expecting, and the widening caused by the inference is probably just going to invite bugs.
Even with the widening the error still seems wrong. I would expect that it fails on the call to isSuccess because the argument type doesn't match the signature (which really highlights my problem with 1). If that's somehow okay, then the narrowing should have worked - in the current version it seems like the value is T assertion is just silently dropped, making the isResult a noop.
When there is an explicitly typed return value, it feels wrong to infer the type on the let, instead of just taking the returned type verbatim. IMO, the explicit return type is a pretty strong signal that you know what you are expecting, and the widening caused by the inference is probably just going to invite bugs.
Consider this modified example:
functiondoWork<T>(x: T): Result<T>{returnx;}constc=doWork(1);// Result<1>letv=doWork(1);// Result<1> widened to Result<number>
In both cases doWork(1) returns a Result<1>, preserving the fact that the argument was the literal 1. The type is widened to Result<number> when we infer it as the type for v because we're inferring for a mutable location (e.g. you might later want to assign a Result<123> to v). The explicitly typed return value isn't really an indicator of intent.
The modified example above works when FAILURE is changed in one of the ways I proposed. However, in your original example, the widening of Result<number> (which is equivalent to number | "FAILURE") ends up producing number | string because the "FAILURE" type is just a string literal type like any other.
Even with the widening the error still seems wrong. I would expect that it fails on the call to isSuccess because the argument type doesn't match the signature (which really highlights my problem with 1). If that's somehow okay, then the narrowing should have worked - in the current version it seems like the value is T assertion is just silently dropped, making the isResult a noop.
What happens is that when inferring from number | string to T | "FAILURE", we infer number | string for T, just as we would have inferred number if you had passed a plain number. Effectively, any type is a valid argument for isSuccess because anything will match T | "FAILURE" as there is no constraint on T.
I was thinking about this issue and what dawned on me is that maybe it will be most intuitive if explicit literal, non-template types are not widened. It's subtle, but the purpose of the union is that the literal options are something you expect and prepare to discriminate on even in mutable locations. Maybe I'm wrong, but I wasn't able to come up with a counter example.
Godfrey Chan (@chancancode)Yehuda Katz (@wycats) We have a fix for this in #11126. Explicit type annotations are now respected and your examples behave as expected. Thanks for the well considered feedback (even if it did arrive as a wall of text 😃).
I'm glad that #11126 helps with examples above - but wow - rules got complicated now. One need to think now in terms of literal types and widening literal types and combination of them and some cases that cancel the widening if there is non-widening form that gets priority :)
reminds me one of this famous CoffeeScript question - "What is the result of :"
foobar and hello world
where users might need to compile the code first to check if it is foo(bar && hello(world)) or foo(bar) && hello(world)
I wish there was a way to satisfy both:
use cases above and
keeping the language easy to reason about - but maybe there is no such way.
One request though would be to add support to TSServer so that it could show users if literal type is detected as widening literal type, because right now for example above it will show:
and one could be surprised that v4 is of type string
@wallverb Yeah, I specifically tried to capture as many subtleties as possible in a short example, and it definitely adds another layer to the rules. I think the intuition is reasonably simple though--as long as an explicit type annotation hasn't been encountered for a particular literal in an expression, that literal type will widen to string when inferred for a mutable location.
@wallverb I'll think about the suggestion of showing widening (or some other indicator) in the hints. In general I'm not crazy about showing syntax that isn't supported in the language, and I don't think the distinction merits elevation to an actual type modifier.
I find it really unfortunate that now more annotations are needed for use-cases where the implicit intent is to use a literal union as a type. I can't formulate a formal proposal, but my strong feeling is that wherever an explicit literal type annotation is given, it should be preserved regardless of mutable or immutable locations.
For example:
typeRel='lt'|'eq'|'gt';functioncompare(x: number,y: number): Rel{return(x===y) ? 'eq' : (x>y) ? 'gt' : 'lt';}constcmpRes=compare(1,2);// `Rel`, okletcmpResMut=compare(1,2);// `string`, what? when did I say that?// for what reason will I ever assign 'hello' to a comparison result?
The second widening is like widening a subclass to the base or any value to Object (as Yehuda Katz (@wycats) pointed out). It allows for unintended values to be assigned and is arguably not intuitive. If this was the intent, than a natural (and a bless for the reader/maintainer) solution is to add the widened annotation by hand.
The same applies to object literals:
constx={left: 1,right: 2,cmp: compare(1,2)// why string?}classA{}classBextendsA{}consty={instance: newB()};// it is correctly `{ instance: B }`
As a side note, this is also a regression in the display of types in IDEs, cmpRes used to be Rel now it is expanded to its members.
Well if that's the case then it's good news, I couldn't get it from the examples. I still have the feeling that apparent literal types are inferred less often after #10676. With #11126 we'll have a mechanism for forcing them at the expense of verbosity (and unfortunately they are not transitive :().
typeRel='lt'|'eq'|'gt';functioncompare(x: number,y: number): Rel{return(x===y) ? 'eq' : (x>y) ? 'gt' : 'lt';}constcmpRes=compare(1,2);// `Rel`, okletcmpResMut=compare(1,2);// `string`, what? when did I say that?// for what reason will I ever assign 'hello' to a comparison result?
Unless I understand wrong, both cmpRes and cmpResMut would have the type Rel under #11126, because compare() explicitly expresses a return type of Rel.
Marin Marinov (@gcnew) Not sure what you mean by not transitive. One thing is certain though: With #10676 and #11126 in place, we'll never infer less specific types for string and numeric literals than we used to. Specifically, we behave just like before when you have explicit type annotations, and whereas we used to immediately widen un-annotated const declarations we now preserve string and numeric literal types from un-annotated const declarations for as long as possible. It's strictly a better thing.
Activity
chancancode commentedon Sep 13, 2016
This is working on 20160911 but broke in 20160912, most likely related to #10676
chancancode commentedon Sep 13, 2016
It seems like the change is somewhat intentional – that
letis getting the "base primitive types" now. However, since the function (doWork, and alsoisSuccessandisFailurehave explicit annotations), I suspect it is possible to keep this pattern working without breaking the other goals?mhegazy commentedon Sep 13, 2016
the change is intentional. see #10676 for more details. I do not think this is a pattern we explicitly thought about; so we need to go back and talk about this.
a work around would be an explicit type annotation to result:
let result: Result<number>chancancode commentedon Sep 13, 2016
Mohamed Hegazy (@mhegazy) keep me posted! 👍 I did try to read #10676, and there seems to be some thoughts/effort in making functions with explicit return type annotation work, so I am crossing my fingers :P
mhegazy commentedon Sep 13, 2016
//cc: Anders Hejlsberg (@ahejlsberg)
ahejlsberg commentedon Sep 14, 2016
The issue here is that when we infer the type
number | "FAILURE"for a mutable location, we can't tell that"FAILURE"is a special type that you don't want widened to string. Indeed, had the type been1 | "FAILURE"you probably would have wanted the1to be widened tonumber.One possible solution is to change the first two lines to:
This turns the
FAILUREtype into an intersection that won't be widened, but otherwise is similar to the literal type"FAILURE". But I think an even better change would be to use an object as the sentinel failure value. That wayResult<T>would also work whenTis a string.chancancode commentedon Sep 14, 2016
I agree the last solution would work and arguably better, but there are still a few things that doesn't feel right to me:
isSuccessbecause the argument type doesn't match the signature (which really highlights my problem with 1). If that's somehow okay, then the narrowing should have worked - in the current version it seems like thevalue is Tassertion is just silently dropped, making theisResulta noop.ahejlsberg commentedon Sep 14, 2016
Consider this modified example:
In both cases
doWork(1)returns aResult<1>, preserving the fact that the argument was the literal1. The type is widened toResult<number>when we infer it as the type forvbecause we're inferring for a mutable location (e.g. you might later want to assign aResult<123>tov). The explicitly typed return value isn't really an indicator of intent.The modified example above works when
FAILUREis changed in one of the ways I proposed. However, in your original example, the widening ofResult<number>(which is equivalent tonumber | "FAILURE") ends up producingnumber | stringbecause the"FAILURE"type is just a string literal type like any other.What happens is that when inferring from
number | stringtoT | "FAILURE", we infernumber | stringforT, just as we would have inferrednumberif you had passed a plainnumber. Effectively, any type is a valid argument forisSuccessbecause anything will matchT | "FAILURE"as there is no constraint onT.ahejlsberg commentedon Sep 14, 2016
I'm re-labeling as Breaking Change as this is working as intended and is an effect of our new approach to better preserving literal types.
gcnew commentedon Sep 15, 2016
I was thinking about this issue and what dawned on me is that maybe it will be most intuitive if explicit literal, non-template types are not widened. It's subtle, but the purpose of the union is that the literal options are something you expect and prepare to discriminate on even in mutable locations. Maybe I'm wrong, but I wasn't able to come up with a counter example.
8 remaining items
ahejlsberg commentedon Sep 24, 2016
Godfrey Chan (@chancancode) Yehuda Katz (@wycats) We have a fix for this in #11126. Explicit type annotations are now respected and your examples behave as expected. Thanks for the well considered feedback (even if it did arrive as a wall of text 😃).
normalser commentedon Sep 24, 2016
I'm glad that #11126 helps with examples above - but wow - rules got complicated now. One need to think now in terms of literal types and widening literal types and combination of them and some cases that cancel the widening if there is non-widening form that gets priority :)
This example
reminds me one of this famous CoffeeScript question - "What is the result of :"
where users might need to compile the code first to check if it is
foo(bar && hello(world))orfoo(bar) && hello(world)I wish there was a way to satisfy both:
One request though would be to add support to
TSServerso that it could show users if literal type is detected as widening literal type, because right now for example above it will show:and one could be surprised that
v4is of typestringso instead of
if it could show:
Thanks a lot for awesome TS
ahejlsberg commentedon Sep 24, 2016
@wallverb Yeah, I specifically tried to capture as many subtleties as possible in a short example, and it definitely adds another layer to the rules. I think the intuition is reasonably simple though--as long as an explicit type annotation hasn't been encountered for a particular literal in an expression, that literal type will widen to
stringwhen inferred for a mutable location.ahejlsberg commentedon Sep 24, 2016
@wallverb I'll think about the suggestion of showing
widening(or some other indicator) in the hints. In general I'm not crazy about showing syntax that isn't supported in the language, and I don't think the distinction merits elevation to an actual type modifier.gcnew commentedon Sep 25, 2016
I find it really unfortunate that now more annotations are needed for use-cases where the implicit intent is to use a literal union as a type. I can't formulate a formal proposal, but my strong feeling is that wherever an explicit literal type annotation is given, it should be preserved regardless of mutable or immutable locations.
For example:
The second widening is like widening a subclass to the base or any value to Object (as Yehuda Katz (@wycats) pointed out). It allows for unintended values to be assigned and is arguably not intuitive. If this was the intent, than a natural (and a bless for the reader/maintainer) solution is to add the widened annotation by hand.
The same applies to object literals:
As a side note, this is also a regression in the display of types in IDEs,
cmpResused to beRelnow it is expanded to its members.ahejlsberg commentedon Sep 25, 2016
Marin Marinov (@gcnew) Agreed. That is exactly the fix we have in #11126.
gcnew commentedon Sep 26, 2016
Well if that's the case then it's good news, I couldn't get it from the examples. I still have the feeling that apparent literal types are inferred less often after #10676. With #11126 we'll have a mechanism for forcing them at the expense of verbosity (and unfortunately they are not transitive :().
wycats commentedon Sep 26, 2016
Unless I understand wrong, both
cmpResandcmpResMutwould have the typeRelunder #11126, becausecompare()explicitly expresses a return type ofRel.ahejlsberg commentedon Sep 26, 2016
Marin Marinov (@gcnew) Not sure what you mean by not transitive. One thing is certain though: With #10676 and #11126 in place, we'll never infer less specific types for string and numeric literals than we used to. Specifically, we behave just like before when you have explicit type annotations, and whereas we used to immediately widen un-annotated
constdeclarations we now preserve string and numeric literal types from un-annotatedconstdeclarations for as long as possible. It's strictly a better thing.gcnew commentedon Sep 27, 2016
My apologies. Literal types now work as expected, thanks :)