Typescript: incorrect result with conditional type and boolean

Created on 12 Oct 2020  路  9Comments  路  Source: microsoft/TypeScript

TypeScript Version: 4.0.2

Search Terms:

"conditional type" "boolean" "incorrect result"

Code

type HasKey<T, K> = K extends keyof T ? true : false
type SimpleNot<X extends boolean> = X extends true ? false : true
type SimpleAnd<A, B> = A extends true ? B extends true ? true : false : false
type IsDisjointSimple<A, B> = SimpleAnd<SimpleNot<HasKey<A, keyof B>>, SimpleNot<HasKey<B, keyof A>>>

type If<Cond extends boolean, Then, Else> = Cond extends true ? Then : Else
type Equal<A, B> =
  (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2)
  ? true : false
type And<A extends boolean, B extends boolean> = If<
  Equal<A, true>,
  B,
  If<
    Equal<B, true>,
    A,
    If<
      Equal<A, boolean>,
      If<Equal<B, boolean>, boolean, false>,
      false
    >
  >
>
type Not<X extends boolean> = If<
  Equal<X, boolean>,
  boolean,
  If<Equal<X, true>, false, true>
>
type IsDisjoint<A, B> = And<Not<HasKey<A, keyof B>>, Not<HasKey<B, keyof A>>>

type Foo = { a: 1 }
type Boo = { b: 1 }

// false - correct
type fooHasBooKeys = HasKey<Foo, keyof Boo>

// true - correct
type fooNoBooKeysSimple = SimpleNot<HasKey<Foo, keyof Boo>> 

// true - correct
type isFooBooDisjointSimpleRaw = And<SimpleNot<HasKey<Foo, keyof Boo>>, SimpleNot<HasKey<Boo, keyof Foo>>> 

// true - correct
type isFooBooDisjointSimple = IsDisjointSimple<Foo, Boo>

// true - correct
type fooNoBooKeys = Not<HasKey<Foo, keyof Boo>>

// true - correct
type booNoFooKeys = Not<HasKey<Boo, keyof Foo>>

// true - correct
type isFooBooDisjointRaw = And<Not<HasKey<Foo, keyof Boo>>, Not<HasKey<Boo, keyof Foo>>> 

// false - wrong
// this should be identical to `isFooBooDisjointRaw`.
// The type is exactly the same.
type isFooBooDisjoint = IsDisjoint<Foo, Boo> 

The type utilities HasKey, If, Equal, And, Not are unit tested in https://github.com/unional/type-plus/pull/71 and should be working as expected.

Playground Link:
playground

Related Issues:

Needs Investigation

All 9 comments

type SimpleNot<X extends boolean> = X extends true ? true : false

I suppose it should be:

type SimpleNot<X extends boolean> = X extends true ? false : true
// false - wrong, should be true.
// `fooHasBooKeys` is narrowed to `false`, not `boolean`,
// so the simple `SimpleNot<X>` should work correctly.
type fooNoBooKeysSimple = SimpleNot<HasKey<Foo, keyof Boo>> 

Then it will work fine.

The change to SimpleNot fixes one of the "wrong"s but not the other. I also tried changing these to be nondistributive but it's not immediately clear whether they even should be or not.

I suppose it should be:

Haha, thanks @Constantiner. You are correct.
That's what happen when trying to come up with a repro in the middle of the night. 馃槢
Code and Playground updated.

@RyanCavanaugh, after updating the example, it shows that the problem does not occur in the Simple scenario (as expected. 馃槅 ).
Could it be the Equal<> causing the problem?. That's one type I borrow from typepark and is the one I have a hard time wrapping my head around. :) I have added tests in to make sure it is working as I expected it to be.

The And and Not types are written this way so that they can handle the boolean type correctly.
e.g. And<boolean, true> returns boolean

I'm pretty sure the problem is in Not thing started with most general (boolean) If. For example this works:

type Not<X extends boolean> = If<
  Equal<X, true>,
  false,
  If<Equal<X, false>, true, If<Equal<X, boolean>, boolean, never>>
>

Um.. I think that won't work. The tricky thing about it is that A, B, X can be boolean and not just a concrete true or false.
So your And will fail in those cases (your code is identical to A extends false ? false : B extends false ? false : true).

Your Not can be simplified to:

  If<
    Equal<X, true>,
    false,
    If<Equal<X, false>, true, boolean>
  >

Your Not and mine are the same because Equal<> takes care of the boolean union type correctly.

The OP code will still be wrong if we just change the Not and not And.

~Here are the tests for And and Not:`~

@Constantiner btw, I like the clean up version of Not more then the one I have.

  If<
    Equal<X, true>,
    false,
    If<Equal<X, false>, true, boolean>
  >

I'll update that in type-plus. Thanks! 馃憤

Actually, I realized that I should propagate boolean in the And and Or case.
Will update the code when I'm done.

Here is the full set of tests for these logical types for reference:

describe('And<A,B>', () => {
  test('basic', () => {
    assertType.isTrue(true as And<true, true>)
    assertType.isFalse(false as And<true, false>)
    assertType.isFalse(false as And<false, true>)
    assertType.isFalse(false as And<false, false>)
  })
  test('boolean special handling', () => {
    assertType.isTrue(true as Equal<And<boolean, true>, boolean>)
    assertType.isFalse(false as And<boolean, false>)
    assertType.isTrue(true as Equal<And<true, boolean>, boolean>)
    assertType.isFalse(false as And<false, boolean>)
    assertType.isTrue(true as Equal<And<boolean, boolean>, boolean>)
  })
})

describe('Or<A,B>', () => {
  test('basic', () => {
    assertType.isTrue(true as Or<true, true>)
    assertType.isTrue(true as Or<true, false>)
    assertType.isTrue(true as Or<false, true>)
    assertType.isFalse(false as Or<false, false>)
  })
  test('boolean special handling', () => {
    assertType.isTrue(true as Or<boolean, true>)
    assertType.isTrue(true as Equal<Or<boolean, false>, boolean>)
    assertType.isTrue(true as Or<true, boolean>)
    assertType.isTrue(true as Equal<Or<false, boolean>, boolean>)
    assertType.isTrue(true as Equal<Or<boolean, boolean>, boolean>)
  })
})

describe('Xor<A,B>', () => {
  test('basic', () => {
    assertType.isFalse(false as Xor<true, true>)
    assertType.isTrue(true as Xor<true, false>)
    assertType.isTrue(true as Xor<false, true>)
    assertType.isFalse(false as Xor<false, false>)
  })
  test('boolean special handling', () => {
    assertType.isTrue(true as Equal<Xor<boolean, true>, boolean>)
    assertType.isTrue(true as Equal<Xor<boolean, false>, boolean>)
    assertType.isTrue(true as Equal<Xor<true, boolean>, boolean>)
    assertType.isTrue(true as Equal<Xor<false, boolean>, boolean>)
    assertType.isTrue(true as Equal<Xor<boolean, boolean>, boolean>)
  })
})

describe('Not<X>', () => {
  test('basic', () => {
    assertType.isTrue(true as Not<false>)
    assertType.isFalse(false as Not<true>)
  })
  test('boolean special handling', () => {
    assertType.isTrue(true as Equal<Not<boolean>, boolean>)
  })
})

Updated the implementation of And in OP and playground

With the And and Or type fixed to propagate boolean, I can reproduce a similar issue with a simplified example:

type If<Cond extends boolean, Then, Else> = Cond extends true ? Then : Else
type Equal<A, B> =
  (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2)
  ? true : false

type And<A extends boolean, B extends boolean> = If<
  Equal<A, true>,
  B,
  If<
    Equal<B, true>,
    A,
    If<
      Equal<A, boolean>,
      If<Equal<B, boolean>, boolean, false>,
      false
    >
  >
>

type Or<A extends boolean, B extends boolean> = If<
  Equal<A, true>,
  true,
  If<
    Equal<B, true>,
    true,
    If<
      Equal<A, false>,
      If<Equal<B, false>, false, boolean>,
      boolean
    >
  >
>

// Y is a pseudo implementation of Xor, just to demo the problem
type Y<A extends boolean, B extends boolean> =
  If<
    Or<Equal<A, boolean>, Equal<B, boolean>>,
    boolean,
    false
  >

// x: false - correct
type x = If<
      Or<Equal<true, boolean>, Equal<true, boolean>>,
      boolean,
      false
    >

// y: boolean - wrong
type y = Y<true, true>

playground

Was this page helpful?
0 / 5 - 0 ratings

Related issues

bgrieder picture bgrieder  路  3Comments

dlaberge picture dlaberge  路  3Comments

MartynasZilinskas picture MartynasZilinskas  路  3Comments

weswigham picture weswigham  路  3Comments

Antony-Jones picture Antony-Jones  路  3Comments