Skip to content
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

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

Closed
CyrusNajmabadi opened this issue Feb 21, 2019 · 43 comments
Closed

Comments

@CyrusNajmabadi
Copy link
Contributor

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?

@CyrusNajmabadi
Copy link
Contributor Author

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.

@CyrusNajmabadi
Copy link
Contributor Author

Tagging @RyanCavanaugh @DanielRosenwasser :)

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Feb 21, 2019

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>

@CyrusNajmabadi
Copy link
Contributor Author

CyrusNajmabadi commented 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
Copy link
Contributor

AnyhowStep commented Feb 21, 2019

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.

@CyrusNajmabadi
Copy link
Contributor Author

CyrusNajmabadi commented 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 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.

@AnyhowStep
Copy link
Contributor

AnyhowStep commented 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.

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())
    }
}

@CyrusNajmabadi
Copy link
Contributor Author

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.

@AnyhowStep
Copy link
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);
    }
}

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Feb 21, 2019

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.

@CyrusNajmabadi
Copy link
Contributor Author

Previously discussed at: #22596

@CyrusNajmabadi
Copy link
Contributor Author

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

Right. Because your typecheck was incorrect.

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Feb 21, 2019

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.

@CyrusNajmabadi
Copy link
Contributor Author

CyrusNajmabadi commented Feb 21, 2019

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.

@AnyhowStep
Copy link
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

@AnyhowStep
Copy link
Contributor

I can create less contrived examples but the idea behind it is the false branch of a type guard

@AnyhowStep
Copy link
Contributor

interface Request {
    //Once a request is completed, it stays completed
    //If it isn't completed, it may complete at some point
    pollCompleted () : boolean;
}

So, if pollCompleted returns true, it will keep returning true forever. If it returns false, it may keep returning false or return true the next time you call it.

Now, our type guard makes sense. If not for () => true, then for { pollCompleted () : true }, at the very least. If we defined the type guard in a file and don't export it and only use it for pollCompleted, there is nothing wrong with a () => true type guard.

Again, the false branch is the problem. It'll narrow to () => false but that is not correct because the request will complete at some point in the future (successful completion, failed completion, timed out completion, etc.) and pollCompleted will return true.

No RNG

@CyrusNajmabadi
Copy link
Contributor Author

IN this case, it seems bogus then for TS to break out boolean as true | false. As that implies that they are equivalent. However, they are not treated as eqiuvalently (as this issue, and some of your examples show). The use of true ends up meaning 'can only be true', whereas true | false was originally the meaning of boolean meaning it could be true *or* false. The operations TS is performing give a stricter view of what boolean actually is.

Imagine it worked this way for numbers, for example. Would you say that if i had number then that would be equivalent to 0 | 1 | 2 | 3 | ...? Would i then end up with Promise<0> | Promise<1> | Promise<2> | ...? And would Promise<number> not be allowed to be assigned to these?

Why is boolean expanded in this manner when it subverts the meaning that boolean is intended to have (namely that it can true or false)?

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Feb 21, 2019

Weird. I thought saw someone post a workaround a while ago but the comment doesn't appear to be around anymore.

The workaround is to change,

T extends primitive ? SimpleInput<T> :

to,

[T] extends [primitive] ? SimpleInput<T> :


Yeah, in this particular case, with what you're actually trying to express, it is unfortunate that conditional types don't work the way you intuitively feel like they should.


number does not distribute that way because there would be way too many elements to work with. So, more of a limitation of memory and speed.

boolean distributes that way because there are only two elements to consider.


type n = Exclude<number, 1>; //number
type b = Exclude<boolean, true>; //false

If boolean didn't distribute in conditional types, b would be boolean and not false.
Would be nice to have negated types for expressing Exclude<number, 1>.


I've run into these gotcha's with conditional types many times, myself!

@CyrusNajmabadi
Copy link
Contributor Author

CyrusNajmabadi commented Feb 22, 2019

number does not distribute that way because there would be way too many elements to work with. So, more of a limitation of memory and speed.

But that's a problem. Imagine memory/speed was not an issue (or TS could represent that large set efficiently). That representation would be wrong, and you'd now see errors like "cannot assign Promise<number> to Promise<0> | Promise<1> | ..." which would make the experience totally sucky :)

It's good that this doesn't happen. But that's why it shouldn't happen for booleans either. Breaking boolean into true | false has made this scenario worse, and we would feel the same if it happened for other discrete sets. It seems fortunate that for implementation reasons we don't :)

@CyrusNajmabadi
Copy link
Contributor Author

Note: the workarounds here have helped a bit, but still fall short. Here's a more complete example:

interface pojo { b: boolean }

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

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

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

declare function promise<T>(val: Input<T>): Promise<T>;

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

declare var bInput: Input<boolean>;
promise(bInput);

We now get an error on promise(bInput) stating:

Argument of type 'SimpleInput<boolean>' is not assignable to parameter of type 'never'.
  Type 'false' is not assignable to type 'never'.

We can't figure out how to get it to actually just infer 'boolean' for the type here, instead of 'true/false' which is throwing it off. Help would be appreciated.

@jack-williams
Copy link
Collaborator

@CyrusNajmabadi

The following works for me, but you need @weswigham's PR #30010 to get the right type inferred for the application of promise.

interface pojo { b: boolean }

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

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

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

declare function promise<T>(val: Input<T>): Promise<T>;

declare var bInput: Input<boolean>;
promise(bInput);

@CyrusNajmabadi
Copy link
Contributor Author

The following works for me,

That works there, but now breaks another piece of code we have:

export function all<T>(val: Record<string, Input<T>>): Promise<Record<string, T>>;
export function all<T1, T2, T3, T4, T5, T6, T7, T8>(values: [Input<T1> | undefined, Input<T2> | undefined, Input<T3> | undefined, Input<T4> | undefined, Input<T5> | undefined, Input<T6> | undefined, Input<T7> | undefined, Input<T8> | undefined]): Promise<[T1, T2, T3, T4, T5, T6, T7, T8]>;
export function all<T1, T2, T3, T4, T5, T6, T7>(values: [Input<T1> | undefined, Input<T2> | undefined, Input<T3> | undefined, Input<T4> | undefined, Input<T5> | undefined, Input<T6> | undefined, Input<T7> | undefined]): Promise<[T1, T2, T3, T4, T5, T6, T7]>;
export function all<T1, T2, T3, T4, T5, T6>(values: [Input<T1> | undefined, Input<T2> | undefined, Input<T3> | undefined, Input<T4> | undefined, Input<T5> | undefined, Input<T6> | undefined]): Promise<[T1, T2, T3, T4, T5, T6]>;
export function all<T1, T2, T3, T4, T5>(values: [Input<T1> | undefined, Input<T2> | undefined, Input<T3> | undefined, Input<T4> | undefined, Input<T5> | undefined]): Promise<[T1, T2, T3, T4, T5]>;
export function all<T1, T2, T3, T4>(values: [Input<T1> | undefined, Input<T2> | undefined, Input<T3> | undefined, Input<T4> | undefined]): Promise<[T1, T2, T3, T4]>;
export function all<T1, T2, T3>(values: [Input<T1> | undefined, Input<T2> | undefined, Input<T3> | undefined]): Promise<[T1, T2, T3]>;
export function all<T1, T2>(values: [Input<T1> | undefined, Input<T2> | undefined]): Promise<[T1, T2]>;
export function all<T>(ds: (Input<T> | undefined)[]): Promise<T[]>;
export function all<T>(val: Input<T>[] | Record<string, Input<T>>): Promise<any> {

We now get:

Overload signature is not compatible with function implementation.ts(2394)

On the second signature. This is def concerning because i can't even figure out why it thinks that.

@CyrusNajmabadi
Copy link
Contributor Author

@weswigham Is there a way to get TS to stop either breaking boolean into true | false, or to stop it distributing type mapping operations over such a union type?

@CyrusNajmabadi
Copy link
Contributor Author

Repro condensed as far as i could make it is here:

interface pojo { b: boolean }

class Output<T> {
    public readonly get: () => T;

    public apply<U>(func: (t: T) => Promise<U>): Output<U>;
    public apply<U>(func: (t: T) => Output<U>): Output<U>;
    public apply<U>(func: (t: T) => U): Output<U>;
    public apply<U>(func: (t: T) => U) { /* will override this in constructor */ return undefined!; }
}

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

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

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

export function all<T>(val: Record<string, Input<T>>): Output<Record<string, T>>;
export function all<T1, T2, T3, T4, T5, T6, T7, T8>(values: [Input<T1> | undefined, Input<T2> | undefined, Input<T3> | undefined, Input<T4> | undefined, Input<T5> | undefined, Input<T6> | undefined, Input<T7> | undefined, Input<T8> | undefined]): Output<[T1, T2, T3, T4, T5, T6, T7, T8]>;
export function all<T1, T2, T3, T4, T5, T6, T7>(values: [Input<T1> | undefined, Input<T2> | undefined, Input<T3> | undefined, Input<T4> | undefined, Input<T5> | undefined, Input<T6> | undefined, Input<T7> | undefined]): Output<[T1, T2, T3, T4, T5, T6, T7]>;
export function all<T1, T2, T3, T4, T5, T6>(values: [Input<T1> | undefined, Input<T2> | undefined, Input<T3> | undefined, Input<T4> | undefined, Input<T5> | undefined, Input<T6> | undefined]): Output<[T1, T2, T3, T4, T5, T6]>;
export function all<T1, T2, T3, T4, T5>(values: [Input<T1> | undefined, Input<T2> | undefined, Input<T3> | undefined, Input<T4> | undefined, Input<T5> | undefined]): Output<[T1, T2, T3, T4, T5]>;
export function all<T1, T2, T3, T4>(values: [Input<T1> | undefined, Input<T2> | undefined, Input<T3> | undefined, Input<T4> | undefined]): Output<[T1, T2, T3, T4]>;
export function all<T1, T2, T3>(values: [Input<T1> | undefined, Input<T2> | undefined, Input<T3> | undefined]): Output<[T1, T2, T3]>;
export function all<T1, T2>(values: [Input<T1> | undefined, Input<T2> | undefined]): Output<[T1, T2]>;
export function all<T>(ds: (Input<T> | undefined)[]): Output<T[]>;
export function all<T>(val: Input<T>[] | Record<string, Input<T>>): Output<any> {
    return undefined!;
}

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


declare function promise<T>(val: Input<T>): Promise<T>;
declare var bInput: Input<boolean>;
promise(bInput);

@jack-williams
Copy link
Collaborator

Are you missing undefined in the implementation signature?

export function all<T>(_val: (Input<T> | undefined)[] | Record<string, Input<T>>): Output<any> {
    return undefined!;
}

@CyrusNajmabadi
Copy link
Contributor Author

possibly... though i don't know why this would change now with the change to to [T] extends [boolean]. The code worked before that change, and i'm concerned this would break downstream consumers of our APIs in unintended ways.

@CyrusNajmabadi
Copy link
Contributor Author

Tried changing it to _val: (Input<T> | undefined)[] | Record<string, Input<T>>

however, that now breaks inside the function. With an error like:

Argument of type 'Input<T> | undefined' is not assignable to parameter of type 'Promise<undefined> | Output<undefined> | (Input<T> extends primitive ? SimpleInput<Input<T>> : Input<T> extends (infer U)[] ? SimpleInput<ArrayOfInputs<U>> : Input<T> extends object ? SimpleInput<InputObject<Input<T>>> : never) | undefined'.
  Type 'Input<T>' is not assignable to type 'Promise<undefined> | Output<undefined> | (Input<T> extends primitive ? SimpleInput<Input<T>> : Input<T> extends (infer U)[] ? SimpleInput<ArrayOfInputs<U>> : Input<T> extends object ? SimpleInput<InputObject<Input<T>>> : never) | undefined'.
    Type 'boolean | Promise<boolean> | Output<boolean> | (T extends primitive ? SimpleInput<T> : T extends (infer U)[] ? SimpleInput<ArrayOfInputs<U>> : T extends object ? SimpleInput<InputObject<T>> : never)' is not assignable to type 'Promise<undefined> | Output<undefined> | (Input<T> extends primitive ? SimpleInput<Input<T>> : Input<T> extends (infer U)[] ? SimpleInput<ArrayOfInputs<U>> : Input<T> extends object ? SimpleInput<InputObject<Input<T>>> : never) | undefined'.
      Type 'false' is not assignable to type 'Promise<undefined> | Output<undefined> | (Input<T> extends primitive ? SimpleInput<Input<T>> : Input<T> extends (infer U)[] ? SimpleInput<ArrayOfInputs<U>> : Input<T> extends object ? SimpleInput<InputObject<Input<T>>> : never) | undefined'.
        Type 'Input<T>' is not assignable to type 'Output<undefined>'.
          Type 'boolean | Promise<boolean> | Output<boolean> | (T extends primitive ? SimpleInput<T> : T extends (infer U)[] ? SimpleInput<ArrayOfInputs<U>> : T extends object ? SimpleInput<InputObject<T>> : never)' is not assignable to type 'Output<undefined>'.
            Type 'false' is not assignable to type 'Output<undefined>'.ts(2345)

So all tehse changes seem to keep kicking the can down the road. The main issue appears to be this extremely strange behavior TS has around the boolean type (which is somewhat surprising since it's such a basic concept).

@jack-williams
Copy link
Collaborator

TBH I'm not actually sure you need the tuple on the [T] extends [boolean] now that the result is explicitly SimpleInput<boolean>.

@CyrusNajmabadi
Copy link
Contributor Author

Also, [T] extends [boolean] breaks a ton more stuff. For example, we had a helper:

export function concat(...params: Input<any>[]): Output<string>

However, this is now being inferred as:

image

In other words, the [T] extends [boolean] is triggering all over the place. i.e. [any] seems to extend [boolean], so the sig is being inferred as only allowing SimpleInput<boolean>[]...

Thbis seems very busted.

@CyrusNajmabadi
Copy link
Contributor Author

CyrusNajmabadi commented Feb 22, 2019

@jack-williams

TBH I'm not actually sure you need the tuple on the [T] extends [boolean] now that the result is explicitly SimpleInput<boolean>.

Do you have a recommendation on what to do instead? Thanks!

@jack-williams
Copy link
Collaborator

jack-williams commented Feb 22, 2019

Yeah.. with [any] you prevent the wildcarding of both sides of the conditional so it just instantly picks the boolean case; you definitely don't want [T] extends [boolean]if you are using Input<any>.

I guess T extends boolean ? SimpleInput<boolean> : seems reasonably given union is idempotent. It's hard for me to be that much help as I don't know the full context, sorry :(.

Re: your issue with the body of all. Is that definitely because of booleans being split? I know the error reports a false type that makes it look like that, but that could just be error elaboration picking out the smallest incompatible type. The conditional type looks pretty gnarly so I wouldn't be completely surprised if there was something else going on there.

@CyrusNajmabadi
Copy link
Contributor Author

I guess T extends boolean ? SimpleInput<boolean> : seems reasonably given union is idempotent.

Unfortunately, that approach doesn't work and causes other errors (listed previously in the thread) :-/

Re: your issue with the body of all. Is that definitely because of booleans being split?

Well, if i remove the special boolean handling, then things work ok. So it seems to be related. Sigh...

@DanielRosenwasser
Copy link
Member

To avoid any being converted to boolean, you should just use T as the parameter since that might not be the most specific type anyway.

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

Are things...less broken with that?

@RyanCavanaugh
Copy link
Member

Can someone give me a TL;DR here?

@RyanCavanaugh RyanCavanaugh self-assigned this Feb 27, 2019
@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label Feb 27, 2019
@CyrusNajmabadi
Copy link
Contributor Author

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

@jack-williams
Copy link
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?

@CyrusNajmabadi
Copy link
Contributor Author

CyrusNajmabadi commented Feb 27, 2019

@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.

@jack-williams
Copy link
Collaborator

jack-williams commented 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

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.

@UrielCh
Copy link

UrielCh commented Oct 15, 2019

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)

@RyanCavanaugh RyanCavanaugh removed the Needs Investigation This issue needs a team member to investigate its status. label Nov 4, 2019
@RyanCavanaugh RyanCavanaugh removed their assignment Nov 4, 2019
@RyanCavanaugh
Copy link
Member

This compiles without error as expected as of 3.5

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Nov 4, 2019

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

@jlennox
Copy link

jlennox commented 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,

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>;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants