Skip to content

TypeScript strangely expanding out boolean into true/false in type alias. #30029

Closed

Description

Using TS 3.3.1. strictNullChecks, noImplicitAny.
Code in question:

interface pojo { b: boolean }

type primitive = Function | string | number | boolean | undefined | null;
type SimpleInput<T> = T | Promise<T>;

type Input<T> =
    T extends primitive ? SimpleInput<T> :
    T extends object ? SimpleInput<InputObject<T>> :
    never;

type InputObject<T> = {
    [P in keyof T]: Input<T[P]>;
}

var ip2: Input<pojo> = { b: Promise.resolve(true) };

Error is reported as:

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?

Activity

  1. CyrusNajmabadi commented on Feb 21, 2019

    @CyrusNajmabadi
    ContributorAuthor

    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.

  2. CyrusNajmabadi commented on Feb 21, 2019

    @CyrusNajmabadi
    ContributorAuthor
  3. AnyhowStep commented on Feb 21, 2019

    @AnyhowStep
    Contributor

    The distribution is coming from T extends primitive.

    Your T is boolean, which is really true|false.

    Conditional types distribute, which gives you,

    true|false|Promise<true>|Promise<false>

    But true|false is simplified to boolean

    And you get boolean|Promise<true>|Promise<false>

  4. CyrusNajmabadi commented on Feb 21, 2019

    @CyrusNajmabadi
    ContributorAuthor

    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.

  5. AnyhowStep commented on Feb 21, 2019

    @AnyhowStep
    Contributor

    Promise<boolean> is not assignable to Promise<true> | Promise<false>.

    declare function isTruePromise (p : Promise<any>) : p is Promise<true>;
    
    const booleanPromise : Promise<boolean> = Promise.resolve(Math.random() > 0.5);
    //Assume this will not error
    const p : 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.

  6. CyrusNajmabadi commented on Feb 21, 2019

    @CyrusNajmabadi
    ContributorAuthor

    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.

  7. AnyhowStep commented on Feb 21, 2019

    @AnyhowStep
    Contributor

    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.

    function b() {
        return Math.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'.
    */
    const f: (() => true) | (() => false) = b;
    
    declare function isTrueFunc(f: any): f is (() => true);
    if (isTrueFunc(f)) {
        for (let i = 0; i < 100; ++i) {
            //Expected to always return true
            console.log(f())
        }
    } else {
        for (let i = 0; i < 100; ++i) {
            //Expected to always return false
            //But we know that it will switch between
            //true and false
            console.log(f())
        }
    }
  8. CyrusNajmabadi commented on Feb 21, 2019

    @CyrusNajmabadi
    ContributorAuthor

    but it doesn't work in the general case.

    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 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 either Promise<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.

  9. AnyhowStep commented on Feb 21, 2019

    @AnyhowStep
    Contributor

    Here's a case where Promise<boolean> is not assignable to Promise<true>|Promise<false>. In function return types.

    function b() {
        return Promise.resolve(Math.random() > 0.5);
    }
    //Pretend this works
    const f: (() => Promise<true>) | (() => Promise<false>) = b;
    
    declare function isTrueFunc(f: any): f is (() => Promise<true>);
    if (isTrueFunc(f)) {
        for (let i = 0; i < 100; ++i) {
            //Expected to always print true
            f().then(console.log);
        }
    } else {
        for (let i = 0; i < 100; ++i) {
            //Expected to always print true
            //But it will switch between true and false
            f().then(console.log);
        }
    }
  10. AnyhowStep commented on Feb 21, 2019

    @AnyhowStep
    Contributor

    You're forgetting the false branch.

    In the false branch, f is now () => false because it is not () => true.

    But it is not () => false because it can return true.

  11. CyrusNajmabadi commented on Feb 21, 2019

    @CyrusNajmabadi
    ContributorAuthor

    Previously discussed at: #22596

  12. CyrusNajmabadi commented on Feb 21, 2019

    @CyrusNajmabadi
    ContributorAuthor

    In the false branch, f is now () => false because it is not () => true.

    Right. Because your typecheck was incorrect.

  13. AnyhowStep commented on Feb 21, 2019

    @AnyhowStep
    Contributor

    The type guard function will return false.

    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.

  14. CyrusNajmabadi commented on Feb 21, 2019

    @CyrusNajmabadi
    ContributorAuthor

    The type guard function will return false.

    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.

  15. AnyhowStep commented on Feb 21, 2019

    @AnyhowStep
    Contributor

    How is it a lie?

    () => Math.random() > 0.5 is not assignable to () => true. The return value could be true or false depending on the PRNG.

    So, if the type guard is working, it will return false

  16. 19 remaining items

  17. RyanCavanaugh commented on Feb 27, 2019

    @RyanCavanaugh
    Member

    Can someone give me a TL;DR here?

  18. CyrusNajmabadi commented on Feb 27, 2019

    @CyrusNajmabadi
    ContributorAuthor

    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 :)

  19. jack-williams commented on Feb 27, 2019

    @jack-williams
    Collaborator

    I got abit lost when trying this incarnation of the type:

    type Input<T> =
        T extends boolean ? SimpleInput<boolean> :
        T extends primitive ? SimpleInput<T> :
        T extends object ? SimpleInput<InputObject<T>> :
        never;

    assuming you have #30010. I think there was something wrong with all?

  20. CyrusNajmabadi commented on Feb 27, 2019

    @CyrusNajmabadi
    ContributorAuthor

    Jack Williams (@jack-williams) You don't really need all to demosntrate the problem. You get the problem right here:

    interface pojo { b: boolean }
    
    type primitive = Function | string | number | boolean | undefined | null;
    type SimpleInput<T> = T | Promise<T>;
    
    type Input<T> =
        T extends primitive ? SimpleInput<T> :
        T extends object ? SimpleInput<InputObject<T>> :
        never;
    
    type InputObject<T> = {
        [P in keyof T]: Input<T[P]>;
    }
    
    var ip2: Input<pojo> = { b: Promise.resolve(true) };

    I've stated that ip2 should be an Input<pojo>. To me, this should expand out like so:

    Input<pojo>    ->
    SimpleInput<InputObject<pojo>>   ->
    SimpleInput<{ b: Input<pojo["b"]> }>   ->
    SimpleInput<{ b: Input<boolean> }>   ->
    SimpleInput<{ b: SimpleInput<boolean> }>   ->
    SimpleInput<{ b: boolean | Promise<boolean> }>    ->
    { b: boolean | Promise<boolean> } | /*... not relevant ...*/
    

    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:

    { b: true | false | Promise<true> | Promise<false> } | /*... not relevant ...*/
    

    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.

  21. jack-williams commented on Feb 27, 2019

    @jack-williams
    Collaborator

    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

    type Input<T> =
        T extends boolean ? SimpleInput<boolean> :
        T extends primitive ? SimpleInput<T> :
        T extends object ? SimpleInput<InputObject<T>> :
        never;

    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.

  22. UrielCh commented on Oct 15, 2019

    @UrielCh

    is that possible to make this code works:

    interface IIsString {
        (Data:String): true;
        (Data:any): false;
    }
    const isString: IIsString = (obj: any) => {
        return typeof obj === 'string' || obj instanceof String;
    }

    the Error:

    Type '(obj: any) => boolean' is not assignable to type 'IIsString'.
    Type 'boolean' is not assignable to type 'true'.ts(2322)

  23. RyanCavanaugh commented on Nov 4, 2019

    @RyanCavanaugh
    Member

    This compiles without error as expected as of 3.5

  24. AnyhowStep commented on Nov 4, 2019

    @AnyhowStep
    Contributor

    Worth noting that the inference only works for object literals.

    interface pojo { b: boolean }
    
    type primitive = Function | string | number | boolean | undefined | null;
    type SimpleInput<T> = T | Promise<T>;
    
    type Input<T> =
        T extends primitive ? SimpleInput<T> :
        T extends object ? SimpleInput<InputObject<T>> :
        never;
    
    type InputObject<T> = {
        [P in keyof T]: Input<T[P]>;
    }
    
    const promiseInObj = { b: Promise.resolve(true) };
    /*
    Type '{ b: Promise<boolean>; }' is not assignable to type 'SimpleInput<InputObject<pojo>>'.
    */
    const ip2: Input<pojo> = promiseInObj;

    Playground

    Just in case someone gets bitten by it.

    If you want to assign { b : Promise<boolean> } to Input<pojo>, you'll still need to ensure the T does not distribute for booleans

  25. jlennox commented on Feb 3, 2022

    @jlennox

    For anyone coming across this in the future (this being the first google result for me):

    My issue, if anyone finds it helpful,

    type Promisify<T> = T extends Promise<any> ? T : Promise<T>;

    would cause Promisify<boolean> to create Promise<true> | Promise<false>

    The correct syntax is:

    type Promisify<T> = [T] extends [Promise<any>] ? T : Promise<T>;
  26. locked as resolved and limited conversation to collaborators on Oct 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions