You can play along in typescript playground
Built in Pick, Omit and other similar type operators don't quite support union types well.
One of the main issues is in keyof. By default when keyof is used with union type it will count only common keys. this is usually not what you want, for example:
type X =
| { type: "string"; foo: string; komara: 1 }
| { type: "number"; foo: number; kzxzdad: string }
| { type: "undefined"; foo: undefined; ajjs: 1; asdad: 44 };
type Keys<T> = keyof T;
// type Keys_X = "type" | "foo"
type Keys_X = Keys<X>;
To fix this we want to use distributive conditional types:
type DistributiveKeys<T> = T extends unknown ? Keys<T> : never;
// type DistributiveKeys_X = "type" | "foo" | "komara" | "kzxzdad" | "ajjs" | "asdad"
type DistributiveKeys_X = DistributiveKeys<X>;
Now we have the foundation we need to rebuild distributive Pick and Omit:
type DistributivePick<T, K extends DistributiveKeys<T>> = T extends unknown
? Pick<T, Extract<keyof T, K>>
: never;
export type DistributiveOmit<T, K extends DistributiveKeys<T>> =
T extends unknown ? Omit<T, Extract<keyof T, K>> : never;
Tho I prefer to inline Omit and Pick which results in much nicer type shown in the IDE. Best way that I've found for doing this, is:
type DistributivePick<T, K extends DistributiveKeys<T>> = T extends unknown
? { [P in keyof Pick_<T, K>]: Pick_<T, K>[P] }
: never;
type Pick_<T, K> = Pick<T, Extract<keyof T, K>>;
export type DistributiveOmit<T, K extends DistributiveKeys<T>> =
T extends unknown ? { [P in keyof Omit_<T, K>]: Omit_<T, K>[P] } : never;
type Omit_<T, K> = Omit<T, Extract<keyof T, K>>;
To see this in action we can compare DistributivePick, a naive implementation of DistributivePick and Pick:
type DistributivePickNaive<T, K extends keyof T> = T extends unknown
? Pick<T, K>
: never;
// type Test1Pick = {
// foo: string | number | undefined;
// type: "string" | "number" | "undefined";
// }
type Test1Pick = Pick<X, "foo" | "type">;
// type Test1Naive =
// | { type: "string"; foo: string; }
// | { type: "number"; foo: number; }
// | { type: "undefined"; foo: undefined; }
type Test1Naive = DistributivePickNaive<X, "foo" | "type">;
// type Test1 =
// | { type: "string"; foo: string; }
// | { type: "number"; foo: number; }
// | { type: "undefined"; foo: undefined; }
type Test1 = DistributivePick<X, "foo" | "type">;
type Test2Pick = Pick<X, "foo" | "type" | "kzxzdad">;
// ERROR ~~~~~~~~~~~~~~~~~~~~~~~~~~
// Type '"type" | "foo" | "kzxzdad"' does not satisfy the constraint '"type" | "foo"'.
// Type '"kzxzdad"' is not assignable to type '"type" | "foo"'.(2344)
type Test2Naive = DistributivePickNaive<X, "foo" | "type" | "kzxzdad">;
// ERROR ~~~~~~~~~~~~~~~~~~~~~~~~~~
// Type '"type" | "foo" | "kzxzdad"' does not satisfy the constraint '"type" | "foo"'.
// Type '"kzxzdad"' is not assignable to type '"type" | "foo"'.(2344)
// type Test2 =
// | { type: "string"; foo: string; }
// | { type: "number"; foo: number; kzxzdad: string; }
// | { type: "undefined"; foo: undefined; }
type Test2 = DistributivePick<X, "foo" | "type" | "kzxzdad">;
Removing {} from result
Sometimes when using those DistributivePick and DistributiveOmit you might get type that contains ... | {} | ... and in some cases you might want to remove it.
type RemoveObject<T> = T extends unknown
? keyof T extends never
? never
: T
: never;
type Test3 =
| {}
| { az: string }
| { foo: 12 }
| {
foo: string;
__foo: string;
};
// type Test3 = {
// az: string;
// } | {
// foo: 12;
// } | {
// foo: string;
// __foo: string;
// }
type Test4 = RemoveObject<Test3>;
Now let's apply this technique to DistributivePick and DistributiveOmit
export type DistributivePick<T, K extends DistributiveKeys<T>> =
T extends unknown
? keyof Pick_<T, K> extends never
? never
: { [P in keyof Pick_<T, K>]: Pick_<T, K>[P] }
: never;
type Pick_<T, K> = Pick<T, Extract<keyof T, K>>;
export type DistributiveOmit<T, K extends DistributiveKeys<T>> =
T extends unknown
? keyof Omit_<T, K> extends never
? never
: { [P in keyof Omit_<T, K>]: Omit_<T, K>[P] }
: never;
type Omit_<T, K> = Omit<T, Extract<keyof T, K>>;
// type Test5 = {
// az: string;
// } | {
// __foo: string;
// }
type Test6 = DistributiveOmit<Test3, "foo">;
// type Test5 = {
// foo: 12;
// } | {
// foo: string;
// }
type Test5 = DistributivePick2<Test3, "foo">;
Originally commented about this here.
Top comments (0)