Pick, Omit and union types in TypeScript
Tags: typescript
Published: 2021-06-09 17:52:53

> You can play along in typescript [playground](https://www.typescriptlang.org/play?#code/PTAECEFcEsBsBdTQHagAYAVoGMDWaAadAeQFtp41QBDZAE1AHt4ALAUwCdQBnac2al3gBPAA5sm4jtXiMO3UHUbIA5IgCOMeBO6RRouYkjJoy0CPEKA7m1iwAdAChHIUMWQTGAM3PtQpahQkbl02BWhw1DRcNmFvNHsIYUU2L2pIBFArdiiYuK8qCNBIbjYGKwoWYpMzCwkKLLhYUGxGUlIzPO5E1iKikshqO2TkZiyWGVA4yCzaeCIvOVA2AA9qUlFYNgAuZ0c60AANUABeUEdQUAAfUABvczEd0AAibngOFABzZ4XGRm2eO8vkRcG1BNQAQBGUAAXwu1zuD3EAOeyEgpAARpwfqBFv9QGjMZwQQAvFYkujUOgAt4fZCfWHwm73Ooo4x0VIoMo4vEA9mcjx0IjUABWIu4UOF3Ep1NAABY5Yz9o9QABpWLcAA8ABUAHynUB5bygbXOVwHdXCbgAfWOZ2edWeCOeeOeyvEao1toNlq1h11e1c2sYuOgK18RRss2QiFkxVKoAA2nQIkCMZB4NAAG4SVr0CimZBDJFhAC6AAoWPB4KIJSArA37HVuNgPqJ4AJ6fY5J9gEpsNxgBN6Bi-rhgBxbGxqKUALSjbSD5ut6Dt2cAJlnAA57FXSLAAMQp2nQdOZnOzvMpzPKIaz5sASl2zgOABFUx8z9m2L6dfqztqywrNo9AKMYuCjFYqAAPyelaf6gACHg5hwADcZpgG+H6nhm36+t69qOs6rrOqCATSE6NzPLgZIUlSlEvKK4oMc8M4ym6WEnl+Ob4XaoDvlxuE8Rqmr+oGYAAHKMFYWQSBMOa+BIizsjIhayQSbBlOYIaTumcAMMeaZCRImA4PgND0CQ5CUM+nFGeebBYHg1o6kQqpASBdAKAJ9l4SJer-iaHlsKB1QQdJyDwrBTm4K5oAAKLAdI2DwJqRo+Nqbm6gGlxIWwKHoY4qwGBwsYqj5n7GWQFAuZlarBaFFU4Q5v4BQagGrJ5YHIOFUFRW41lxYl7zUClaWxMadWqtl8J5QV4kmiwIYAJKgKIk5eJw2lIMgsBclZFBULQDCmXgVDZDgVSTroCCRP4kDYFUJjYFtBzcEtUE7YpoDLa+8XPkVKwlWVHpNdxjlmXF7mdSFXn8dh4Otdl7UNXD4GQZFlywfciYYF96WgDFtVZaWAJE1Duq46WjK5Rp83uhI5NTYFMVDUlo2pQTzO6oVxWGCW8OCQ51WpVNqPeQjxlI4FHXAbD3W9ZjoDY0meNBATIvE2quqkwNNUU1TNOIXTnCFQcmsUwaItsyNY1c1lPN7C4YDBjwmkRndHNqVG2C0C0bSiIIJlg8ZMWEDQBKBApfCbGwpAhfAqlmMaaAhw5YcWSdYe7OcDOC75OYxRJUcSFD4uGhNGUy+X6MRecWOIrjX2qrr2qGzCoCuCge2CqAKG8MnPinbFzNoLNJtoQtBzamE8CQjFBq3M7lwr7ygJ0gyNyEliXA3PyXhcnQ6GuCvJYoie9Isdv2LOvvh-PMfYBwtPs-z2ZBqs4cRAun8LGOgGy8X5vEhMXb8pxl7MjPi8C+3xUK4j+DSIE9I4FwlcJA1kLxr4cAfvA-EWCUEQMRBg54d9BQ4LXqQsoBCgFz1AQpM4advxFxLqJb+JEqL-1zuaFUM9gHgLQUQx458kGwNwYgjeBCBEsiEZg9EO9yEIIJHI02jIpFQJIfQAU3I4EUM0QfQU1CeGvwNIwwukMv4vHYS8ThewaHrgXmcT+bDf7OiIlRGi5J2IBlcPFAASr44gvjT7BNPgAP3CREyJUTolROXtqFUKgHSPBYlY6itF2IqEUIwMIBIxjcFUtwLwyRWC5mULSQIMZQCJLcZY3+KgnAnxNAktJnj6KZKKAuGgIRoCfCLBiLY20DjVOScROp9hyzrgAMwKgfC+Ixbx1x0IkAwyW6czJLNYbUxgf8RnuPSfRbxYA-EBKCSEs55zgkxKudc8JcTmk1J-ts0i+y6DPEyUoHJnT8mZkKcUvweZykoEQMM8QKSxnL0uPEj0iSPF0Vee0hQnSZy8F6dQfpEg4xDKSaC0Z2z6kTOmXKWZgD5nwHXPwsA6CZGvBEQo-EMDJGUsEciWRRJsE6MUfgw0LzxFfEZQiaRLKNEcn0dosR1QRWH0MR6XhZKTGrKYeY5xTyOG7JeLCrxC1iAfE+CgIYsBkitHaAnLSaLGAZg9kmdgk4KxVhrHWYAurWCQAxPYI1wByCtkYNwbw8BgBQrYAAZRXO2YAERQiDnXFuSZkyACcB5w2QDYEa+OMZtyTKjVuSEW4ADskJiV518XHRgOZiAYhFMm0W1cYahVrn1U+sEubl2QpweEITYIto4G24JAJTSnzmq2vOsrJmLw7syagJJtgwLHXcXkkJ1wztuLyGBRBrTWmXSIuEGEBbDsXhCleE7eXIOXou-dlw17zsfrCAVZ7xUMtvWute97XDP1JYqM4RaOilvLZWnUs9JkAL5qVAWpiIZ4HXGXGtcNQPSxRlBhWGN+oEyZllZt+VW0Nont2gEOM1aoGQ2ZLW01dYoe1u3cenbeZA35nZSqwtrIQbFvB-OdG-LwTagBGuPVEMNw1tZIj+pmOdu7R29DXb+2NzwxXfIet4ACd1hbZm5HaaUanqSgArHuxplxD3rz5Sem92nQCPsUc+p+Q7Z4ADZ5VC2-CLRj-6CCPOeAAklMrZ6abOEvIzF71xXtPT50zIj-MWbeJ5ljzVFXgb-W8SZTnXS6iAA)

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:

```ts
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](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#distributive-conditional-types):

```ts
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`:

```ts
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:

```ts
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`:

```ts
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.

```ts
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`

```ts
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](https://github.com/microsoft/TypeScript/issues/28339#issuecomment-832881871).