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'>
Return home to Smooth Unfolding