60

I'm new to typescript and I have a few interfaces defined like so:

interface A {
  toRemove: string;
  key1: "this1";
  key2: number;
}
interface B {
  toRemove: string;
  key1: "this2";
  key3: string;
}

And a union of both interfaces:

type C = A|B;

What I want to do is to remove the toRemove key from both interfaces through C, something like this:

type CC = Omit<A, "toRemove">|Omit<B, "toRemove">;

But without having to omit the key from both interfaces. This would be ideal:

type CC = Omit<C, "toRemove">;

But, unfortunately, CC will be of type Pick<A|B, "key1">, where key1 is the key present in both interfaces.

In essence, what I'm trying to achieve is a type of "function" to transform:

A1|A2|...|An

into:

Omit<A1, K keyof A1>|Omit<A1, K keyof A2>|...|Omit<An, K keyof An>

I came across this answer https://stackoverflow.com/a/56297816/6520174 and I have a feeling that part of what I need is somewhere in there, but I don't really understand what's going on in that code.

Luis Pais
  • 703
  • 1
  • 6
  • 6

2 Answers2

99

You want to distribute the Omit across a union. Luckily, you can use distributive conditional types to achieve this:

type DistributiveOmit<T, K extends keyof any> = T extends any
  ? Omit<T, K>
  : never;

The T extends any construction looks like it doesn't do much, but since T is a type parameter, it distributes the conditional type across any union constituents of T.

Let's test it:

type CC = DistributiveOmit<C, "toRemove">;
// type CC = Pick<A, "key1" | "key2"> | Pick<B, "key1" | "key3">

You can verify that this is equivalent to the CC type you want.

Link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • 2
    I've set my editor to warn me on the use of any types and I believe that they can be removed in this case. Can `K extends keyof any` be replaced with `K extends keyof T`, and couldn't `T extends any` be replaces with `T extends T`, since it'll be resolved to `A extends T?... | B extends T?...` – Luis Pais Jul 19 '19 at 09:22
  • 1
    You can replace `keyof any` with `PropertyKey` if you want; replacing it with `keyof T` may meet your use cases, but the [standard library's definition of `Omit`](https://github.com/Microsoft/TypeScript/blob/v3.5.3/lib/lib.es5.d.ts#L1466-L1469) uses `keyof any` so that's why I used it. As for `T extends any`, you probably could replace it with `T extends T` but I haven't used that construct. I suspect it would be resolved to `A extends A ? ... | B extends B ? ...`. My instinct would be to use `T extends unknown` instead. – jcalz Jul 19 '19 at 14:59
  • `PropertyKey` and `T extends unknown` are perfect ! Even though the standard library uses `keyof any`, I'll use this just to remove properties from types/interfaces, so `PropertyKey` will suffice. I'm going to read more on the implications of using `unknown`. Thanks for all the help. – Luis Pais Jul 19 '19 at 15:15
-1

Another approach is:

export declare type Without<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
type CC = Without<A, "toRemove">| Without<B, "toRemove">;
  • 1
    Thanks but this won't do, your `Without` is basically Typescript's `Omit`. I also said in the question: `without having to omit the key from both interfaces`. Clearly, your solution requires removing the key from both interfaces. – Luis Pais Jul 19 '19 at 08:53