hand sketched logo of electrons orbiting a nucleus

TIL: Distributive Conditional Types

Using conditional matching, we can distribute a type over a union of types.

Here is our simple union of types:

type Union = 'a' | 'b' | 'c';

If we wanted to take an action on each type in the union, we can use a conditional type:

type DblString<T extends string> = T extends any ? `${T}${T}` : never;

type DblUnion = DblString<Union>; // 'aa' | 'bb' | 'cc'

This is such a simple union example that we could have done this same thing with an IIMT:

type DblStringIIMT<K, T> = {
  [K in Union]: `${K}${K}`;
}[Union];

type DblUnionIIMT = DblStringIIMT<Union>; // 'aa' | 'bb' | 'cc'

What if we had a more complex union of types?

Then our distributive conditional type would still work:

type ComplexUnion =
  | {
      first: 'John';
      last: 'Doe';
    }
  | {
      first: 'Jane';
      last: 'Doe';
    }
  | {
      first: 'John';
      last: 'Smith';
    }
  | {
      first: 'Jane';
      last: 'Smith';
    };

type DistributedOmit<T, TOmit extends PropertyKey> = T extends any
  ? Omit<T, TOmit>
  : never;

type JustFirstNames = DistributedOmit<ComplexUnion, 'last'>;
// Omit<{ first: 'John'; last: 'Doe'; }, 'last'> | Omit<{ first: 'Jane'; last: 'Doe'; }, 'last'> | Omit<{ first: 'John'; last: 'Smith'; }, 'last'> | Omit<{ first: 'Jane'; last: 'Smith'; }, 'last'>

While, the IIMT version (1) becomes coupled to the ComplexUnion type and (2) requires a unique key. You can see how messy this is getting to write:

type DistributedOmitIIMT<
  T extends Record<'first' | 'last' | string, PropertyKey>,
  TOmit extends PropertyKey,
> = {
  [K in T as `${K['first']}${K['last']}`]: Omit<K, TOmit>;
}[`${T['first']}${T['last']}`];

type JustFirstNamesIIMT = DistributedOmitIIMT<ComplexUnion, 'last'>;
// Omit<{ first: 'John'; last: 'Doe'; }, 'last'> | Omit<{ first: 'Jane'; last: 'Doe'; }, 'last'> | Omit<{ first: 'John'; last: 'Smith'; }, 'last'> | Omit<{ first: 'Jane'; last: 'Smith'; }, 'last'>