Omit in TypeScript isn’t distributive
Imagine you have this type X.
type X = {
readonly x: string;
} & (
| {
readonly type: 'Y';
readonly y: number;
}
| {
readonly type: 'Z';
readonly z: boolean;
}
);
And you want to define a type similar to X but without property x. You might think you can get it like this.
type X2 = Omit<X, 'x'>;
But when you try to use it, you’ll find that the complier complains.
const o: X2 = {
type: 'Y',
y: 1,
};
Why? When you expand X2, you’ll find it’s like this. As you can see, it doesn’t have neither y nor z.
type X2 = {
readonly type: 'Y' | 'Z';
};
This is because Omit isn’t distributive, and only gets the common properties of all the union types.
As you may have known, keyof X isn’t 'x' | 'type' | 'y' | 'z', but 'x' | 'type'. Omit<X, 'x'> has Exclude<keyof X, 'x'> as keys. So it’ll only have type property.
You can define a distributive version of Omit like this.
type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
DistributiveOmit<X, 'x'> will be expanded to a union of each type without x.
DistributedOmit<X, 'x'> =
| Omit<{
readonly x: string;
} & {
readonly type: 'Y';
readonly y: number;
}>
| Omit<{
readonly x: string;
} & {
readonly type: 'Z';
readonly z: boolean;
}>;
So this definition is valid.
const o: DistributiveOmit<X, 'x'> = {
type: 'Y',
y: 1,
};
Note that you can have a distributive version of keyof this way.
type DistributiveKeyOf<t> = T extends unknown ? keyof T : never;
then, DistributiveKeyOf<X> will be expanded to 'x' | 'type' | 'y' | 'z'.