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'>