hckr.fyi // thoughts

TypeScript Intersection and Union Types

by Michael Szul on

A lot of my tutorials and blog posts lately have been specifically Bot Framework-oriented. What you may have noticed, however, is that I'm using the Node.JS SDK and not the .NET SDK. I've been focusing on programming chatbots in JavaScript. Well… not exactly JavaScript, but TypeScript.

You probably already know what TypeScript is, so I won't rehash that here. The reason I chose to use TypeScript with the Node.JS SDK for my chatbot work is that TypeScript gives me that static typing that I like in C#, but the Node.JS SDK for the Bot Framework is more conducive to quick prototyping. I see it as getting the best of both worlds.

A lot of people understand the core concepts of TypeScript and the need for static typing. The language services embedded in various IDEs alone have saved people countless hours of debugging (myself included), but a lot of times, people forget about some of the more advanced types for special case scenarios when you want to be more flexible, but still have some form of static typing.

This is where TypeScript offers advanced types such as the intersection type and the union type.

Intersection Types

The intersection type combines types, so that you can have all properties and members of both types on your particular object or method. In Metron, I use this for an extension of the prototype for the Element object. Metron has a convenience method called attribute, which either returns a string value of the attribute your are attempting to access, or it sets a string value for that attribute, but then returns the Element for which you are setting the attribute, so that you can chain methods.

Element.prototype.attribute = function(name: string, value?: string): string & Element {
        if(value != null) {
            this.setAttribute(name, value);
            return this;
        }
        return this.getAttribute(name);
    };
    

There's nothing really to the code above. It's meant as a convenience method to have both setAttribute() and getAttribute() in the same method off of the Element object, while allowing for chaining since setAttribute() doesn't have a return value.

We use the intersection type here so that we have the language service recognize that the return value could either be a string or the Element object itself.

Union Types

Union types, meanwhile, are an either/or scenario. Instead of combining types to receive all members and properties, we use union types to specify that a method accepts either of several different types.

For example, in the following code--pulled from Metron's pivot functionality--the target parameter can either be a number or a string. In the case of a number, it gets the pivot from the index, but in the case of a string, it gets the pivot based on the name of the actual control.

public exact(target: number | string): boolean {
            var self = this;
            var idx;
            if(isNaN(<any>target)) {
                for(let i = 0; i < self._items.length; i++) {
                    let page = self._items[i].attribute("data-m-page");
                    if(page != null && page == target) {
                        idx = i;
                    }
                }
                if(idx == null) {
                    throw new Error(`Error: Cannot find a pivot with page name ${target}`);
                }
            }
            else {
                idx = target;
            }
            if(!self._items[idx]) {
                console.log(`Error: No pivot at index ${idx}`);
                return false;
            }
            self.applyPreEvent(self._item.current);
            for(let i = 0; i < self._items.length; i++) {
                self._items[i].hide();
            }
            self._items[idx].show();
            if(self._items[idx].attribute("data-m-page") != null) {
                metron.routing.setRouteUrl(self._items[idx].attribute("data-m-page"), "", true);
            }
            self.init(self._items[idx]);
            return true;
        }
    

Union types also allow you to simplify code by using them in conjunction with the type keyword to create your own advanced types.

type AjaxRequest = string | JSON | XMLDocument;
    

In the above code, AjaxRequest could return a string, a JSON object, or an XMLDocument. This allows us to use this type anywhere that a response from an AJAX request is returned.

There's a lot more you can do with intersection and union types. These are just a few simple examples, but the important point to get across, is that a lot of times, these advanced types get lost in the shuffle when building out TypeScript code, since they aren't something that native JavaScript developers are used to.