Create Union in TypeScript

How to you create an extensible union in TypeScript?

For one of my recent projects I wanted to introduce some flexibility by offering extensibility. Offering extensibility in TypeScript can be done in multiple ways, most notably we have:

  • Generics (powerful, transparent, explicit)
  • Declaration merging (more restrictive, hidden, implicit)

I first went for generics, but then realized that offering more and more extension points will result in a total bloat of type arguments. At the end I introduced declaration merging as an easy way out.

The theory is this: We introduce a type (interface) such as Foo which extends another (empty) interface called CustomFoo. This interface sits at a "known" location and can then be modified by plugins:

declare module 'base-lib/known-location' {
  interface CustomFoo {
    // put new stuff in here
  }
}

Great! Now that works flawlessly and from any number of plugins / sources.

There is just one drawback / question that comes up. My project required unions on some positions. Mostly, these unions are used as discriminated unions - but that does not matter much. Let's pretend I have the following defined:

export interface HtmlComponent {
  type: 'html';
  render: RenderType;
}

export interface ReactComponent {
  type: 'react';
  render: RenderType;
}

export type AnyComponent = HtmlComponent | ReactComponent;

How can I enable plugins to extend AnyComponent, e.g., one plugin to bring in a VueComponent? How can the union be extended?

Let's start with what we know. We know that interface can easily be extended.

export interface Components extends CustomComponents {
  html: HtmlComponent;
  react: ReactComponent;
}

The CustomComponent interface would be empty and placed on a "known" location - like beforehand. So far so good. But how can we combine the properties of the Components interface to form a union?

The answer to this lies in the index operator - which can be used in types dynamically.

export type UnionOf = { [K in keyof T]: T[K] }[keyof T];

export type AnyComponent = UnionOf;

Together with type guards and conditional types this forms a deadly combination that gives us great possibilities!

Created .

References

Sharing is caring!