Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Negating typescript types?

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.

like image 723
Magnus Avatar asked Aug 11 '18 03:08

Magnus


1 Answers

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 argument A | B | C for T 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!

like image 131
jcalz Avatar answered Oct 03 '22 19:10

jcalz