Let's say I have the following discriminated unions and some associated types
type Union = 'a' | 'b';
type Product<A extends Union, B> = { f1: A, f2: B};
type ProductUnion = Product<'a', 0> | Product<'b', 1>;
Now I can take complements by using mapping types and Exclude
type UnionComplement = {
[K in Union]: Exclude<Union, K>
};
// {a: "b"; b: "a"}
type UnionComplementComplement = {
[K in Union]: Exclude<Union, Exclude<Union, K>>
};
// {a: "a"; b: "b"}
So far all of this makes sense but things break down for ProductUnion
when I try to take the double complement. The first complement works fine
type ProductComplement = {
[K in Union]: Exclude<ProductUnion, { f1: K }>
};
// {a: Product<'b', 1>; b: Product<'a', 0>}
The double complement is incorrect no matter what I try
type ProductComplementComplement = {
[K in Union]: Exclude<ProductUnion, Exclude<ProductUnion, { f1: K }>>
};
// {a: ProductUnion; b: ProductUnion}
I don't understand where the bug is because if I substitute the types then it should work. There are only 2 values for K
when taking the double complement so let's try the first one
type First = Exclude<ProductUnion, Exclude<ProductUnion, { f1: 'a' }>>;
// {f1: 'a'; f2: 0}
Second one also work
type Second = Exclude<ProductUnion, Exclude<ProductUnion, { f1: 'b' }>>;
// {f1: 'b'; f2: 1}
All the constituent parts work but when combined in the mapping type it seems to break down. What am I missing here?
On a whim I tried adding a type parameter to see what would happen by abstracting the complementing process
type Complementor<T> = {
[K in Union]: Exclude<T, { f1: K }>
};
type DoubleComplementor<T> = {
[K in Union]: Exclude<T, Exclude<T, { f1: K }>>
};
Now if I apply the parametrized types to ProductUnion
it works exactly as I expect
type Complement = Complementor<ProductUnion>;
// {a: Product<'b', 1>; b: Product<'a', 0>}
type DoubleComplement = DoubleComplementor<ProductUnion>;
// {a: Product<'a', 0>; b: Product<'b', 0>}
This was indeed a bug: https://github.com/Microsoft/TypeScript/issues/28824. Thanks to Anders and team the next release should have more consistent behavior.