3
\$\begingroup\$

There was this type challenge:

Get an Object that is the difference between O & O1

I came up with such solution but I wonder if you have advice to simplify my approach?

My solution:

type Diff<O extends object, O1 extends object, tmp extends object ={[k in keyof O]: k extends keyof O1 ? never:O[k]} &{[k in keyof O1]: k extends keyof O ? never:O1[k]}  > = Filter<tmp,NonNeverKeys<tmp>>

Where

type NonNeverKeys<T extends object > = {
  [K in keyof T]-?: T[K] extends never ? never : K
}[keyof T];

and

type Filter<T extends object, V extends keyof T> = {
  [K in V]-?: T[K]};

Explanation:

  • If you do Diff<Foo, Bar> then tmp holds (Foo,Bar below):

      {
          name: never;
          age: never;
      } & {
          name: never;
          age: never;
          gender: number;
      }
    

Then I use my helpers to first extract key names which aren't never from above intersection, and then filter above intersection using extracted keys.

Test cases:

type Foo = {
  name: string
  age: string
}
type Bar = {
  name: string
  age: string
  gender: number
}
type Coo = {
  name: string
  gender: number
}

type cases = [
  Expect<Equal<Diff<Foo, Bar>, { gender: number }>>,
  Expect<Equal<Diff<Bar, Foo>, { gender: number }>>,
  Expect<Equal<Diff<Foo, Coo>, { age: string; gender: number }>>,
  Expect<Equal<Diff<Coo, Foo>, { age: string; gender: number }>>,
]

I tried to solve without using helper TS types like Exclude etc.

\$\endgroup\$

2 Answers 2

3
\$\begingroup\$

You can make this easier by using the Venn diagram

Venn diagram

Let's say we have two sets A and B. All you need is the union of A - B and B - A. How can we get them if we only know A and B?

First of all we need to find two things: the union () of A and B and the intersection () of A and B. Why?

As we see in the diagram

  • A = (A - B) ∪ A∩B
  • B = (B - A) ∪ A∩B

Now we can see that

  • A - B = A - A∩B
  • B - A = B - A∩B

We need union of A - B and B - A: (A - B)∪(B - A) = A∪B - A∩B - A∩B. In terms of sets, subtraction the same thing twice is useless so we can subtract only one time, so the final formula:

(A - B)∪(B - A) = A∪B - A∩B

In TypeScript the union of two types can be defined with & operator:

type Union<T1, T2> = T1 & T2;

The intersection of two types can be defined in this way (original post):

type Intersection<T1, T2> = {
  [K in keyof T1 & keyof T2]: T1[K] | T2[K]
}

So all we need is use Omit to get the difference

type Diff<T1, T2> = Omit<Union<T1, T2>, keyof Intersection<T1, T2>>

Example

UPD

Sorry I didn't notice this "I tried to solve without using helper TS types like Exclude". It is not a problem we can define our type MyOmit

In the source of Omit we can see that Omit defined as:

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>

So we need to define our MyExclude and MyPick to define MyOmit as:

type MyOmit<T, K extends keyof any> = MyPick<T, MyExclude<keyof T, K>>

As it described in source we can define MyExclude like this:

type MyExclude<T, U> = T extends U ? never : T

MyPick we also can define as it described in source:

type MyPick<T, K extends keyof T> = {
    [P in K]: T[P];
}

I know that you can say "Hey! You just copied the code from source it is not your implementation!" and you will be absolutely right. But I don't think that I must to invent a bicycle :) It is better to understand how it works and then use it anywhere

Lets start with the easiest type MyPick:

  1. Pick<T, K extends keyof T> here we use K extends keyof T to define that we can pass only the keys which are in type T

  2. [P in K] it works similar as in JS loop for..in, we just list object keys

  3. T[P] means that for each key P we define exactly the type which defined in type T

The type MyExclude also not so hard but it will be easier to understand on specific example. Lets say that T = 'a' | 'b' | 'c' and U = 'a' | 'b'

Then we have to deal with 3 cases:

  1. 'a' extends 'a' | 'b' ? never : T - yes 'a' extends 'a' | 'b', so the answer is never
  2. 'b' extends 'a' | 'b' ? never : T - yes 'b' extends 'a' | 'b', so the answer is never
  3. 'c' extends 'a' | 'b' ? never : T - no 'c' extends 'a' | 'b', so the answer is 'c'

Now we have:

MyExclude<T, U> = MyExclude<'a' | 'b' | 'c', 'a' | 'b'> = never | never | 'c' = 'c'

The final code:

type Union<T1, T2> = T1 & T2;

type Intersection<T1, T2> = {
  [K in keyof T1 & keyof T2]: T1[K] | T2[K]
}

type MyPick<T, K extends keyof T> = {
    [P in K]: T[P];
};

type MyExclude<T, U> = T extends U ? never : T;

type MyOmit<T, K extends keyof any> = MyPick<T, MyExclude<keyof T, K>>

type Diff<T1, T2> = MyOmit<Union<T1, T2>, keyof Intersection<T1, T2>>

Example

\$\endgroup\$
0
\$\begingroup\$

OP here posted question as guest. I found one solution which indirectly uses Excludes kind of logic after as:

type Diff<O extends object, O1 extends object,  > = FilterNever<{[k in keyof O]: k extends keyof O1 ? never:O[k]} &{[k in keyof O1]: k extends keyof O ? never:O1[k]} >
 
type FilterNever<Type> = {
    [Property in keyof Type as Type[Property] extends never ? never:Property]: Type[Property]
};
\$\endgroup\$
3
  • 2
    \$\begingroup\$ You have presented an alternative solution, but haven't reviewed the code. Please edit to show what aspects of the question code prompted you to write this version, and in what ways it's an improvement over the original. It may be worth (re-)reading How to Answer. \$\endgroup\$ Jan 24, 2023 at 15:05
  • \$\begingroup\$ Is john, i.e. the OP of the question, another unregistered account you have? If you register then you can use the contact SE page and request the accounts be merged. \$\endgroup\$ Jan 24, 2023 at 16:34
  • \$\begingroup\$ Welcome to the Code Review Community. While this might be a good answer on stackoverflow, it is not a good answer on Code Review. A good answer on Code Review contains at least one insightful observation about the code. Alternate code only solutions are considered poor answers and may be down voted or deleted by the community. Please read How do I write a good answer. \$\endgroup\$
    – pacmaninbw
    Jan 24, 2023 at 20:52

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.