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