Strict Unions in Typescript – A Practical Approach

Strict Unions in Typescript – A Practical Approach

Unions are great and working with them in typescript is a piece of cake in most cases. That is, until you stumble upon the following problem: you must define all the possible combinations of unions, which may come into your component and all the union combinations must have all properties of all the other unions, only undefined.

This may sound complicated, but let me explain. 

What are strict unions? 

Strict unions are not a very well-known concept within the programming community, and it’s not pretty clear where the term ’strict‘ comes from. After searching the term in the google engine, there isn’t much coming up, at least not at the point of writing this article. Nevertheless, they offer an elegant solution to specific types of problems which may occur during software development.

At its core, strict unions are evaluated combinations of possible properties which can be obtained after running the following three lines of code:

type UnionKeys<T> = T extends T? keyof T : never;
type StrictUnionHelper<T, TAll> = T extends T? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, undefined>> : never;
type StrictUnion<T> = StrictUnionHelper<T, T>

This may still sound complicated, but let me explain on a more practical example.

Practical use

So, how could those three lines of code come in handy?

I recently had a situation where I had to map some data from a headless CMS to a component in Next.js application. I had to define what kind of properties can be received by the mapping component. The mentioned component would display itself in one of three variations, depending on the received properties:

Variation 1 (content mode)

Variation 2 (pros and cons)

Variation 3 (teaser image with content)

The groups of possible properties for each variation which may come into the Next.js component were as follows:

It was easy to notice, that the properties horizontalSize and backgroundColor with their definitions are common across all the possible groups of properties, and the next step was to define all the properties common to all variations and the unique ones belonging to specific groups.

I came up with the following type to pass into the mapping component:

After passing the type to the component as typescript component property definition I got the following error:

And it makes sense for this to happen. After all, if any of the possible combinations of properties get evaluated, all the other properties should not be received by the component. But then again, if they are not received by the component, they cannot be mapped into the component. I had to come up with a better solution.

One of the possible solutions was to define all the possible combinations as follows:

And pass those combinations into the component like this:

But this solution seems a bit off. Firstly, there is plenty of code. Secondly, there are lines of code which repeat themselves over and over again. Imagine having 10 possible combinations and how that would look like. Thankfully, that is where strict unions come in handy and in particular the definition on the start of the page. By passing CaasInfoBoxProps from the first approach into the StrictUnion<T> as T, typescript is now able to deduce all the necessary properties with fewer lines of code.

The possible combinations of properties after using StrictUnion<T> type:

({
  backgroundColor: BackgroundColors;
  horizontalSize: number;
} & {
  mode: InfoBoxMode.CONTENT;
  text_content: string;
  headline: string;
} & Partial<Record<"teaser_image" | "teaser_image_alt" | "teaser_image_webp" | "contra" | "pro", undefined>>) |
({
  backgroundColor: BackgroundColors;
  horizontalSize: number;
} & {
  mode: InfoBoxMode.PRO_CONTRA;
  pro?: string[];
  contra?: string[];
} & Partial<Record<"headline" | "teaser_image" | "teaser_image_alt" | "teaser_image_webp" | "text_content", undefined>>) |
({
  backgroundColor: BackgroundColors;
  horizontalSize: number;
} & {
  mode: InfoBoxMode.TEASER;
  headline: string;
  text_content: string;
  teaser_image?: string;
  teaser_image_alt?: string;
  teaser_image_webp: string;
} & Partial<Record<"contra" | "pro", undefined>>)

Conclusion

Although not a very well-known concept to many, the practical appliance of the StrictUnion<T> type may come in handy and benefit one in many ways, like:

  • Reducing needed lines of code
  • Making prettier type definitions
  • Reducing needed time to define the possible combinations of properties

Schreibe einen Kommentar