Home > front end >  Is `distributive conditional types` the desired behavior typically? Why?
Is `distributive conditional types` the desired behavior typically? Why?

Time:11-17

https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types

Typically, distributivity is the desired behavior. To avoid that behavior, you can surround each side of the extends keyword with square brackets.

Is distributive conditional types the desired behavior typically? What are the use cases?

CodePudding user response:

It's because distributivity over unions is often the most natural behavior for type operations.

Let's say you have a function like

declare function f<T>(x: T): F<T>;

which takes a value of type T and returns a value of type F<T> where F is some generic type function. If you have a value a of some type A, and a value b of some type B, then f(a) will be of type F<A> and f(b) will be of type F<B>:

declare function f<T>(x: T): F<T>;
const fa = f(a); // F<A>
const fb = f(b); // F<B>

Now, what happens when you pass either a or b to f() and assign the result to a variable?

const fab = f(Math.random() < 0.5 ? a : b); // F<A | B>

The input is of type A | B, so the output must be F<A | B> by definition. On the other hand, what happens if you either call f(a) or f(b) and assign the result to a variable?

const fafb = Math.random() < 0.5 ? f(a) : f(b); // F<A> | F<B>

Well, the result must be F<A> | F<B> by definition.

But compare fab to fafb; for any reasonable implementation of f(), you would predict the exact same set of possible values for fab and fafb, right? fab is the result of f(a) or f(b) since the input is either a or b, and fafb is also the result of f(a) or f(b). That implies that the type F<A | B> and the type F<A> | F<B> should be the same type.

This is exactly what it means to distribute a type function over a union. A type function F<> distributes over unions if and only if F<T | U> is equivalent to F<T> | F<U> for all types T and U.


So then, any use case where you represent a data transformation with a type operation will probably be one where you would like that type operation to be distributive over unions. Let's examine some common distributive type operations in TypeScript.

Indexed access types are distributive over unions:

interface I { x: string, y: number };
type IX = I["x"]; // string
type IY = I["y"]; // number
type IXY = I["x" | "y"]; // string | number
type IXIY = I["x"] | I["y"]; // string | number

type Also = ({ a: string } | { a: number })["a"] // string | number

When mapped types are homomorphic (where you are mapping with in keyof as introduced in ms/TS#12447 when it was called isomorphic, see this question/answer for more info), they are distributive over unions:

type HomMap<T> = { [K in keyof T]: (x: T[K]) => void };
type MA = HomMap<{ a: string }>; // type MA = {  a: (x: string) => void; }
type MB = HomMap<{ b: string }>; // type MB = {  b: (x: string) => void; }
type MAB = HomMap<{ a: string } | { b: string }> 
  // type MAB = HomMap<{ a: string; }> | HomMap<{ b: string; }> 
type MAMB = HomMap<{ a: string; }> | HomMap<{ b: string; }> 
  // type MAMB = HomMap<{ a: string; }> | HomMap<{ b: string; }> 

And, of course, conditional types where the checked type is a generic type parameter are distributive:

type DCT<T> = T extends { a: string } ? number : string;
type DA = DCT<{ a: string }> // number
type DB = DCT<{ b: string }> // string
type DAB = DCT<{ a: string } | { b: string }>
// type DAB = string | number
type DADB = DCT<{ a: string }> | DCT<{ b: string }>
// type DADB = string | number

At this point you might wonder which type operations should not distribute over unions. This concept is strongly tied with variance (see this q/a for more info). Type functions that are covariant, such as those representing property reads or data outputs, should be distributive over unions. Type functions that are contravariant, such as those representing property writes or data inputs, should also distribute after a fashion, but turn unions into intersections. But quite often you will get invariant type functions, such as those representing arbitrary operations, and in these cases you don't want to distribute over unions. So it is sometimes useful to be able to turn off distributivity. I won't belabor the point showing examples here, though.

Playground link to code

  • Related