-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Comments
Note: even if ths is intentional, and TS expands out |
Tagging @RyanCavanaugh @DanielRosenwasser :) |
The distribution is coming from Your Conditional types distribute, which gives you,
But And you get |
@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. |
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. |
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 IN the case of your code if the wrong thing runs, it's because your |
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 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())
}
} |
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 Heck, this is why it's safe to map |
Here's a case where 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);
}
} |
You're forgetting the In the But it is not |
Previously discussed at: #22596 |
Right. Because your typecheck was incorrect. |
The type guard function will return There is no way You want it to be possible for Because this But this is not right. It is still This only happened because we assigned Assigning |
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 |
How is it a lie?
So, if the type guard is working, it will return |
I can create less contrived examples but the idea behind it is the false branch of a type guard |
So, if Now, our type guard makes sense. If not for Again, the No RNG |
IN this case, it seems bogus then for TS to break out Imagine it worked this way for numbers, for example. Would you say that if i had Why is boolean expanded in this manner when it subverts the meaning that boolean is intended to have (namely that it can |
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,
to,
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.
type n = Exclude<number, 1>; //number
type b = Exclude<boolean, true>; //false If I've run into these gotcha's with conditional types many times, myself! |
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 It's good that this doesn't happen. But that's why it shouldn't happen for booleans either. Breaking boolean into |
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
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. |
The following works for me, but you need @weswigham's PR #30010 to get the right type inferred for the application of 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); |
That works there, but now breaks another piece of code we have:
We now get:
On the second signature. This is def concerning because i can't even figure out why it thinks that. |
@weswigham Is there a way to get TS to stop either breaking |
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); |
Are you missing export function all<T>(_val: (Input<T> | undefined)[] | Record<string, Input<T>>): Output<any> {
return undefined!;
} |
possibly... though i don't know why this would change now with the change to to |
Tried changing it to however, that now breaks inside the function. With an error like:
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). |
TBH I'm not actually sure you need the tuple on the |
Also,
However, this is now being inferred as: In other words, the Thbis seems very busted. |
Do you have a recommendation on what to do instead? Thanks! |
Yeah.. with I guess Re: your issue with the body of |
Unfortunately, that approach doesn't work and causes other errors (listed previously in the thread) :-/
Well, if i remove the special boolean handling, then things work ok. So it seems to be related. Sigh... |
To avoid 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? |
Can someone give me a TL;DR here? |
@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 I'm happy to dive into anything here to clarify further :) |
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 |
@jack-williams You don't really need 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
Based on that, i should be able to assign
While expanding 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 type Input<T> =
T extends boolean ? SimpleInput<boolean> :
T extends primitive ? SimpleInput<T> :
T extends object ? SimpleInput<InputObject<T>> :
never; was proposed that prevents |
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'. |
This compiles without error as expected as of 3.5 |
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; Just in case someone gets bitten by it. If you want to assign |
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 The correct syntax is: type Promisify<T> = [T] extends [Promise<any>] ? T : Promise<T>; |
Using TS 3.3.1. strictNullChecks, noImplicitAny.
Code in question:
Error is reported as:
It's unclear to me why/how this is happening. The expansion of
Input<pojo.b>
seems to have becomeboolean | Promise<true> | Promise<false>
instead ofboolean | Promise<boolean>
.is this expected? a bug? If intentional, is there some sort of workaround?
The text was updated successfully, but these errors were encountered: