Extending typescript intersection with optional properties
January 18th, 2023Typescript 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!