Extending typescript intersection operator with optional properties

Typescript provides us very powerful operators to extend our existing types: union type and intersection type. Let's quickly cover what they are and how are different. Of course, for more in depth examples i encourage you to read the official documentation.

Union

A union type is a type formed from two or more other types, representing values that may be any one of those types. We refer to each of these types as the union’s members.

Intersection

An intersection type combines multiple types into one. This allows you to add together existing types to get a single type that has all the features you need

Let's say we have to interfaces and we want to combine them to create a new type.

One option is to use the intersection:

interface A {
    firstName: string
    lastName: string
}

interface B {
    age: number
}

type C = A & B

As a result, C will inherit all properties from A and B, and they all will required.

Another option is to use union

interface A {
    firstName: string
    lastName: string
}

interface B {
    age: number
}

type C = A | B

As a result, C will inherit only the common properties between A and B, and they will also be required. In the case of C type, that will be no properties.

Combining them for something more advanced

What if we need something in between these two results? Recently i had the need to create a type that contained all common properties between two interfaces as required properties and the rest as optional properties. I played for a little while with the tools that typescript provides and i was able to come up with something that did what i needed. Let's see how we can build such type.

First, let's create a type with only common keys between the two interfaces. This line is saying that the resulting type C will have a key for each one that appears in A and B.

interface A {
    firstName: string
    lastName: string
}

interface B {
    age: number
}

type C = {
    [K in keyof A & keyof B]: A[K] | B[K]
}

Then, we use the typescript's utility Exclude to take out from A all the keys that also appear in B, leaving out the uniques to A and we specify the type that key had in A. By adding the question mark we make them optional.

interface A {
    firstName: string
    lastName: string
}

interface B {
    age: number
}

type C = {
    [K in Exclude<keyof A, keyof A & keyof B>]?: A[K]
}

We can do the same thing for B now

interface A {
    firstName: string
    lastName: string
}

interface B {
    age: number
}

type C = {
    [K in Exclude<keyof B, keyof A & keyof B>]?: b[K]
}

Now, using the intersection operator, we can combine them all into one type that will fulfill our requirement: the common keys to A and B will be required and the rest optional.

interface A {
    firstName: string
    lastName: string
}

interface B {
    age: number
}

type C = {
    [K in keyof A & keyof B]: A[K] | B[K]
} & {
    [K in Exclude<keyof A, keyof A & keyof B>]?: A[K]
} & {
    [K in Exclude<keyof B, keyof A & keyof B>]?: b[K]
}

Next steps

To wrap up, we can create our own utility type so we can reuse that we have done.

/**
 * Construct a type with the properties common to T and U as required properties and the rest as optional properties
 */
type SoftIntersection<T, U> = {
    [K in keyof T & keyof U]: T[K] | U[K]
} & {
    [K in Exclude<keyof T, keyof T & keyof U>]?: T[K]
} & {
    [K in Exclude<keyof U, keyof T & keyof U>]?: U[K]
}

I've decided to name this utility SoftIntersection as i wanted to differentiate it from the normal intersection operator, but better names are welcomed!