I wanted to create a simple NOT operator in typescript where you get all primitives combined into the union of some type A that are NOT primitive members of the union of a second type B. This can be done using conditional types. For example, if you have types:
type A = 'a' | 'b' | 'c';
type B = 'c' | 'd' | 'e';
... then I want to map them to a third derived type [A - B] that, in this case, would yield:
type C = 'a' | 'b'
This seems to be doable using conditionals of the form shown below. HOWEVER, I am completely stumped why the NOT operator below seems to give me what I want, but explicitly spelling out the exact same conditional logic does not:
type not_A_B_1 = A extends B ? never : A; // 'a' | 'b' | 'c'
type Not<T, U> = T extends U ? never : T;
type not_A_B_2 = Not<A, B> // 'a' | 'b'
See here.
Could someone pls tell me if I'm missing some TS subtlety here that would explain why not_A_B_1
and not_A_B_2
are not equivalent? Thanks.
You've run into distributive conditional types:
Conditional types in which the checked type is a naked type parameter are called distributive conditional types. Distributive conditional types are automatically distributed over union types during instantiation. For example, an instantiation of
T extends U ? X : Y
with the type argumentA | B | C
forT
is resolved as(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)
So in this:
type not_A_B_1 = A extends B ? never : A; // 'a' | 'b' | 'c'
The A
is a concrete type, not a type parameter, so the conditional type does not get distributed over its constituents.
But in
type Not<T, U> = T extends U ? never : T;
the T
is a naked type parameter, so the conditional type does get distributed. What does "naked" mean? It means T
as opposed to some type function of T
. So in {foo: T} extends W ? X : Y
, the type parameter T
is "clothed", so it doesn't distribute.
This leads to a way to turn off distributive conditional types when you don't want them: clothe the type parameter. The simplest and least verbose way to do this is to use a tuple of one element: So,
T extends U ? V : W // naked T
[T] extends [U] ? V : W // clothed T
Since [T] extends [U]
should be true exactly when T extends U
, those are equivalent except for the distributivity. So let's change Not<>
to be non-distributive:
type NotNoDistribute<T, U> = [T] extends [U] ? never : T;
type not_A_B_2 = NotNoDistribute<A, B> // 'a' | 'b' | 'c'
Now not_A_B_2
is the same as not_A_B_1
. If you prefer the original not_A_B_2
behavior, then use distributive conditional types like Not<>
. If you prefer the undistributive behavior, use concrete types or clothed type parameters. Does that make sense?
By the way, your Not<T,U>
type is already present as a predefined type in the standard library as Exclude<T,U>
, meant to "exclude from T
those types that are assignable to U
".
Hope that helps. Good luck!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With