Veetaha
Veetaha

Reputation: 862

TypeScript tuples conditional comparison always evaluates to false

This is very straightforward, here is the link to playground

As I have noticed, when you remove tuple type from condition in If<> everything goes as expected. But I don't want to do it (t2 assertion blows), moreover I'd like to understand wy this happens. Is this just a bug? Why And<> type always expands to true?

type If<
    TCond extends boolean,
    TIfTrue,
    TElse
> = [TCond] extends [true] ? TIfTrue : TElse; // if you remove tuples, it works 

type Not<T extends boolean> = If<(T), false, true>;

type IsNever<TSuspect> = TSuspect extends never ? true : false;

type AssertFalse<TSuspect extends false> = TSuspect;
type AssertTrue <TSuspect extends true> = TSuspect;

//   V~~ always true 
type And<T extends boolean[]> = Not<Not<IsNever<Extract<T[number], false>>>>;

type AndExpected<T extends boolean[]> = IsNever<Extract<T[number], false>>;


type t0 = AssertFalse<Not<true>>;  
type t1 = AssertTrue<Not<false>>;  
type t2 = AssertTrue<Not<boolean>>;

type t3 = AssertFalse<And<[false]>>; // ????
type t4 = AssertFalse<AndExpected<[false]>>;

Upvotes: 2

Views: 444

Answers (1)

artem
artem

Reputation: 51609

First, I'll try to explain why If with tuples differs from If without tuples:

type If<
    TCond extends boolean,
    TIfTrue,
    TElse
> = [TCond] extends [true] ? TIfTrue : TElse; // if you remove tuples, it works 

type If2<
    TCond extends boolean,
    TIfTrue,
    TElse
> = TCond extends true ? TIfTrue : TElse;  


type Not<T extends boolean> = If<(T), false, true>;
type Not2<T extends boolean> = If2<(T), false, true>;

// with tuples, Not<boolean> is true because that's how you set it up
type A1 = Not<boolean>; // true
type A2 = Not<true>;    // false
type A3 = Not<false>;   // true

// without typles, TCond extends true is distributive over union types,
// and boolean is really just a union of true | false,
// so Not2<boolean> is boolean 
type B1 = Not2<boolean>; // boolean 
type B2 = Not2<true>;    // false
type B3 = Not2<false>;   // true

ok, now to the question. Here's the strange thing I (and Titian Cernicova-Dragomir) found:

type SameAsT<T extends boolean> = Not<Not<T>>;
type X1 = SameAsT<false>; // true !
type X2 = SameAsT<true>; // true again

// but why?

If you expand all the literal types, it works as expected:

type ReallyNotFalse = [false] extends [true] ? false : true; // true
type ReallyNotNotFalse = [([false] extends [true] ? false : true)] extends [true] ? false : true; // false

Looks like a bug in the compiler.

Incidentally, Not based on If without tuples works as expected

type SameAsT2<T extends boolean> = Not2<Not2<T>>;
type Y1 = SameAsT2<false>; // false
type Y2 = SameAsT2<true>; // true 

So, it's possible to use some other way to suppress distributive union type in conditions and to make the code in question work. One way is to add superfluous condition which always evaluates to true, and does not have TCond as the type being checked:

type If<
    TCond extends boolean,
    TIfTrue,
    TElse
> = {} extends TCond ? TCond extends true ? TIfTrue : TElse : never; 

type Not<T extends boolean> = If<(T), false, true>;

type IsNever<TSuspect> = TSuspect extends never ? true : false;

type AssertFalse<TSuspect extends false> = TSuspect;
type AssertTrue <TSuspect extends true> = TSuspect;

//   V~~ always true 
type And<T extends boolean[]> = Not<Not<IsNever<Extract<T[number], false>>>>;

type AndExpected<T extends boolean[]> = IsNever<Extract<T[number], false>>;


type t0 = AssertFalse<Not<true>>;  
type t1 = AssertTrue<Not<false>>;  
type t2 = AssertTrue<Not<boolean>>;

type t3 = AssertFalse<And<[false]>>; 
type t4 = AssertFalse<AndExpected<[false]>>;

Upvotes: 1

Related Questions