Search code examples
typescripttyping

Make non overlapping property of interface/type optional


I want to "merge" multiple interfaces/types in Typescript. The outcoming interface should have all shared property mandatory and all "not-shared" propertys optional. Here an example:

interface ShopUser {
   userId: string
   boughtItems: string[]
}

interface BlogUser {
   userId:string
   viewedBlogs: string[]
}

interface AppUser {
   userId: string
   appSessions: string[]
}

The Outcome should be something like this:

interface DBUser {
   userId: string          // mandatory, because all childs have this property
   boughtItems?:string[]   // optional now
   viewedBlogs?:string[]   // optional now
   appSessions?:string[]   // optional now
}

I already tried these things:

type DBUser = ShopUser | BlogUser | AppUser // now only userId is usable. The Rest is unknown

type DBUser = ShopUser & BlogUser & AppUser // now all Properties are mandatory...

Using something like Omit<ShopUser, "boughtItems"> and then retyping seems somehow not elegant to me and since our Interfaces are much more complex it also ends in a mess and is not very reusable.


Solution

  • In a union A | B, the accessible keys are those that are shared between A and B. Therefore, keyof (A | B) gives us the shared keys. The type should be A[K] | B[K] or (A | B)[K].

    For the keys that aren't shared, we can just take the intersection A & B and omit the shared keys to get the difference of the two types. Then we can add Partial to make all the properties optional.

    The resulting type is too complex and TypeScript will display the uncomputed type. You can force TypeScript to "evaluate" it using a conditional type and a mapped type.

    type Merge<A, B> = {
        [K in keyof (A | B)]: A[K] | B[K]; // shared keys
    } & ( // intersect with
        Partial<Omit<A & B, keyof (A | B)>> // the difference
            extends infer O ? { [K in keyof O]: O[K] } : never // pretty display
    );
    

    Merge all three types now:

    type DBUser = Merge<Merge<ShopUser, BlogUser>, AppUser>;
    

    Playground link


    It is also possible to write a type Merge such that it takes a tuple of types and merges them in the same way:

    type DBUser = Merge<[ShopUser, BlogUser, AppUser]>;
    

    But that is something for the reader to figure out ;')