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
{{ message }}
Repository navigation
TypeScript strangely expanding out boolean into true/false in type alias. #30029
Type '{ b: Promise<boolean>; }' is not assignable to type 'SimpleInput<InputObject<pojo>>'.
Type '{ b: Promise<boolean>; }' is not assignable to type 'InputObject<pojo>'.
Types of property 'b' are incompatible.
Type 'Promise<boolean>' is not assignable to type 'boolean | Promise<false> | Promise<true>'.
Type 'Promise<boolean>' is not assignable to type 'Promise<false>'.
Type 'boolean' is not assignable to type 'false'.
It's unclear to me why/how this is happening. The expansion of Input<pojo.b> seems to have become boolean | Promise<true> | Promise<false> instead of boolean | Promise<boolean>.
is this expected? a bug? If intentional, is there some sort of workaround?
Note: even if ths is intentional, and TS expands out boolean to true | false in something like a mapped type, then it seems super strange to me that Promise<boolean> is not compatible with ... | Promise<true> | Promise<false>. Promise<boolean> could only be Promise<true> | Promise<false> so these types should be complimentary AFAICT.
My general complaint is that even with that expansion, it seems overly onerous that TS then doesn't consider the type compatible. Despite it appearing that there would be no reasonable way to observe a difference.
Promise<boolean> is not assignable to Promise<true> | Promise<false>.
declarefunctionisTruePromise(p : Promise<any>) : p is Promise<true>;constbooleanPromise : Promise<boolean>=Promise.resolve(Math.random()>0.5);//Assume this will not errorconstp : Promise<true>|Promise<false>=booleanPromise;if(isTruePromise(p)){p.then(console.log);//Expected to ALWAYS print `true`}else{p.then(console.log);//Expected to ALWAYS print `false`}
However, we can see that neither expectation can be satisfied.
I honestly don't get your example. Why would neither expectation be satisfied? For a promise, once its value is resolved, it can't change. So, a Promise<boolean> is literally only able, at the end of the day, to finally be a Promise<true> or a Promise<false>. i.e. Promise<true> | Promise<false>.
IN the case of your code if the wrong thing runs, it's because your isTruePromise actually lied and didn't properly assess what type of Promise this was. But that's not TS's fault. it would be hte same as any sort of other incorrect truthy check which TS just trusted because it was an : foo is bar checking function.
Fair enough. I forgot that the resolved value doesn't change.
But the type system doesn't know that. It cannot assume that a value does not change between invocations. You can argue that it should be assignable for Promise<boolean>, but it doesn't work in the general case.
functionb(){returnMath.random()>0.5;}//Pretend this works/*Type '() => boolean' is not assignable to type '(() => true) | (() => false)'. Type '() => boolean' is not assignable to type '() => true'. Type 'boolean' is not assignable to type 'true'.*/constf: (()=>true)|(()=>false)=b;declarefunctionisTrueFunc(f: any): f is (()=>true);if(isTrueFunc(f)){for(leti=0;i<100;++i){//Expected to always return trueconsole.log(f())}}else{for(leti=0;i<100;++i){//Expected to always return false//But we know that it will switch between//true and falseconsole.log(f())}}
Yes, if you make a lying type-check function, all bets are off. That is the case today. That's why Promise<true> | Promise<false> is equiv to Promise<true | false>. Let's say you have the former, each time you do anything with it, you have to assume it could be eitherPromise<true> or Promise<false>. That means that any usages of it's values would have to be acceptable given either of those interpretations. Turns out this means you need to be able to handle true or false. which is the same contract that Promise<true|false> give you.
Heck, this is why it's safe to map boolean to true|false in the first place.
Here's a case where Promise<boolean> is not assignable to Promise<true>|Promise<false>. In function return types.
functionb(){returnPromise.resolve(Math.random()>0.5);}//Pretend this worksconstf: (()=>Promise<true>)|(()=>Promise<false>)=b;declarefunctionisTrueFunc(f: any): f is (()=>Promise<true>);if(isTrueFunc(f)){for(leti=0;i<100;++i){//Expected to always print truef().then(console.log);}}else{for(leti=0;i<100;++i){//Expected to always print true//But it will switch between true and falsef().then(console.log);}}
There is no way function b is () => true. We agree on that.
The type guard function is not malfunctioning.
You want it to be possible for () => boolean to be assignable to (() => true)|(() => false) but I'm showing you that this leads to undesirable results.
Because this (() => true)|(() => false) = b is not (() => true), the type guard will return false.
And because it returns false, the type of the function is now narrowed to (() => false).
But this is not right. It is still () => boolean.
This only happened because we assigned () => boolean to (() => true)|(() => false). Not because the type guard is wrong.
Assigning () => boolean to (() => true)|(() => false) is the only incorrect typecheck here.
I'm saying: that is a lie as well. Your function cannot make a claim one way or the other. it would be the same as if you had a system which could encode numbers, and you had a typecheck that said this function will return a value > 10, but you then ran that on a random number generating function. It would not be legal to say either true or false. You cannot make a claim in any direction about this.
Ryan Cavanaugh (@RyanCavanaugh) OP lays out what i would expect to work pretty simply. I've been brought up to speed on some of the strangeness you can get with union types and conditional types with naked type paramters. However, i def feel like the strangeness is somewhat undesirable. I'm not sure if you guys can improve things here... but it seems really unfortunate that i can be trying to express something so simple, but end up with TS interpretting it in such a strange manner that simple code fails to work.
I'm happy to dive into anything here to clarify further :)
Based on that, i should be able to assign { b: Promise.resolve(true) }. However, this is disallowed because Promise.resolve(true) gives me Promise<boolean> (should it instead give me Promise<true>?), but the actual expanded type is something more like:
While expanding boolean to true | false in b: boolean isn't a problem (since you can still use any boolean, or hte literal true/false types), it is a problem that it expanded Promise<boolean> out to Promise<true> | Promise<false>. These types are not equivalent, and hte latter is far more restrictive and limiting.
This is especialy problematic as in my type signatures i never once mentione the true/false types. I just operated on booleans. And yet, i'm left with an expanded type that does not allow booleans.
EDIT: I'll add that I appreciate that the sentiment of the issue is really about boolean and distribution, and not just about working around the problem. But if it's not possible to change the behaviour, I think it's important to get some canonical examples and workarounds.
Yes I follow all that bit, but I was mainly trying to understand the workarounds. I think the type
was proposed that prevents Promise<true> | Promise<false>, but then there was issues with later type-checking for all I believe? This comment. Why that broke is where I got lost.
Activity
CyrusNajmabadi commentedon Feb 21, 2019
Note: even if ths is intentional, and TS expands out
booleantotrue | falsein something like a mapped type, then it seems super strange to me thatPromise<boolean>is not compatible with... | Promise<true> | Promise<false>.Promise<boolean>could only bePromise<true> | Promise<false>so these types should be complimentary AFAICT.CyrusNajmabadi commentedon Feb 21, 2019
Tagging Ryan Cavanaugh (@RyanCavanaugh) Daniel Rosenwasser (@DanielRosenwasser) :)
AnyhowStep commentedon Feb 21, 2019
The distribution is coming from
T extends primitive.Your
Tisboolean, which is reallytrue|false.Conditional types distribute, which gives you,
true|false|Promise<true>|Promise<false>But
true|falseis simplified tobooleanAnd you get
boolean|Promise<true>|Promise<false>CyrusNajmabadi commentedon Feb 21, 2019
AnyhowStep I addressed that here: #30029 (comment)
My general complaint is that even with that expansion, it seems overly onerous that TS then doesn't consider the type compatible. Despite it appearing that there would be no reasonable way to observe a difference.
AnyhowStep commentedon Feb 21, 2019
Promise<boolean>is not assignable toPromise<true> | Promise<false>.However, we can see that neither expectation can be satisfied.
CyrusNajmabadi commentedon Feb 21, 2019
I honestly don't get your example. Why would neither expectation be satisfied? For a promise, once its value is resolved, it can't change. So, a
Promise<boolean>is literally only able, at the end of the day, to finally be aPromise<true>or aPromise<false>. i.e.Promise<true> | Promise<false>.IN the case of your code if the wrong thing runs, it's because your
isTruePromiseactually lied and didn't properly assess what type of Promise this was. But that's not TS's fault. it would be hte same as any sort of other incorrect truthy check which TS just trusted because it was an: foo is barchecking function.AnyhowStep commentedon Feb 21, 2019
Fair enough. I forgot that the resolved value doesn't change.
But the type system doesn't know that. It cannot assume that a value does not change between invocations. You can argue that it should be assignable for
Promise<boolean>, but it doesn't work in the general case.CyrusNajmabadi commentedon Feb 21, 2019
Again, that's because your isTrueFunc is lying :)
Yes, if you make a lying type-check function, all bets are off. That is the case today. That's why
Promise<true> | Promise<false>is equiv toPromise<true | false>. Let's say you have the former, each time you do anything with it, you have to assume it could be eitherPromise<true>orPromise<false>. That means that any usages of it's values would have to be acceptable given either of those interpretations. Turns out this means you need to be able to handletrueorfalse. which is the same contract thatPromise<true|false>give you.Heck, this is why it's safe to map
booleantotrue|falsein the first place.AnyhowStep commentedon Feb 21, 2019
Here's a case where
Promise<boolean>is not assignable toPromise<true>|Promise<false>. In function return types.AnyhowStep commentedon Feb 21, 2019
You're forgetting the
falsebranch.In the
falsebranch,fis now() => falsebecause it is not() => true.But it is not
() => falsebecause it can return true.CyrusNajmabadi commentedon Feb 21, 2019
Previously discussed at: #22596
CyrusNajmabadi commentedon Feb 21, 2019
Right. Because your typecheck was incorrect.
AnyhowStep commentedon Feb 21, 2019
The type guard function will return
false.There is no way
function bis() => true. We agree on that.The type guard function is not malfunctioning.
You want it to be possible for
() => booleanto be assignable to(() => true)|(() => false)but I'm showing you that this leads to undesirable results.Because this
(() => true)|(() => false) = bis not(() => true), the type guard will returnfalse.And because it returns
false, the type of the function is now narrowed to(() => false).But this is not right. It is still
() => boolean.This only happened because we assigned
() => booleanto(() => true)|(() => false). Not because the type guard is wrong.Assigning
() => booleanto(() => true)|(() => false)is the only incorrect typecheck here.CyrusNajmabadi commentedon Feb 21, 2019
I'm saying: that is a lie as well. Your function cannot make a claim one way or the other. it would be the same as if you had a system which could encode numbers, and you had a typecheck that said
this function will return a value > 10, but you then ran that on a random number generating function. It would not be legal to say either true or false. You cannot make a claim in any direction about this.AnyhowStep commentedon Feb 21, 2019
How is it a lie?
() => Math.random() > 0.5is not assignable to() => true. The return value could betrueorfalsedepending on the PRNG.So, if the type guard is working, it will return
false19 remaining items
RyanCavanaugh commentedon Feb 27, 2019
Can someone give me a TL;DR here?
CyrusNajmabadi commentedon Feb 27, 2019
Ryan Cavanaugh (@RyanCavanaugh) OP lays out what i would expect to work pretty simply. I've been brought up to speed on some of the strangeness you can get with
union typesandconditional types with naked type paramters. However, i def feel like the strangeness is somewhat undesirable. I'm not sure if you guys can improve things here... but it seems really unfortunate that i can be trying to express something so simple, but end up with TS interpretting it in such a strange manner that simple code fails to work.I'm happy to dive into anything here to clarify further :)
jack-williams commentedon Feb 27, 2019
I got abit lost when trying this incarnation of the type:
assuming you have #30010. I think there was something wrong with
all?CyrusNajmabadi commentedon Feb 27, 2019
Jack Williams (@jack-williams) You don't really need
allto demosntrate the problem. You get the problem right here:I've stated that
ip2should be anInput<pojo>. To me, this should expand out like so:Based on that, i should be able to assign
{ b: Promise.resolve(true) }. However, this is disallowed becausePromise.resolve(true)gives mePromise<boolean>(should it instead give mePromise<true>?), but the actual expanded type is something more like:While expanding
booleantotrue | falseinb: booleanisn't a problem (since you can still use any boolean, or hte literal true/false types), it is a problem that it expandedPromise<boolean>out toPromise<true> | Promise<false>. These types are not equivalent, and hte latter is far more restrictive and limiting.This is especialy problematic as in my type signatures i never once mentione the true/false types. I just operated on booleans. And yet, i'm left with an expanded type that does not allow booleans.
jack-williams commentedon Feb 27, 2019
EDIT: I'll add that I appreciate that the sentiment of the issue is really about boolean and distribution, and not just about working around the problem. But if it's not possible to change the behaviour, I think it's important to get some canonical examples and workarounds.
Yes I follow all that bit, but I was mainly trying to understand the workarounds. I think the type
was proposed that prevents
Promise<true> | Promise<false>, but then there was issues with later type-checking forallI believe? This comment. Why that broke is where I got lost.UrielCh commentedon Oct 15, 2019
is that possible to make this code works:
the Error:
Type '(obj: any) => boolean' is not assignable to type 'IIsString'.
Type 'boolean' is not assignable to type 'true'.ts(2322)
RyanCavanaugh commentedon Nov 4, 2019
This compiles without error as expected as of 3.5
AnyhowStep commentedon Nov 4, 2019
Worth noting that the inference only works for object literals.
Playground
Just in case someone gets bitten by it.
If you want to assign
{ b : Promise<boolean> }toInput<pojo>, you'll still need to ensure theTdoes not distribute for booleansjlennox commentedon Feb 3, 2022
For anyone coming across this in the future (this being the first google result for me):
My issue, if anyone finds it helpful,
would cause
Promisify<boolean>to createPromise<true> | Promise<false>The correct syntax is: