TypeScript’s Three Musketeers: Conquer Code Chaos with Discriminated Unions, Asserts & Namespace Overriding

Max Lloyd
4 min readMay 7, 2023

--

Discriminated unions

Discriminated unions give you the power to handle multiple states and shapes of your data, with grace. I’ll give a quick example of using it without vs with in a fictional messaging application.

Without

Let’s take a look at this code

type Message = {
type: 'MESSAGE' | 'GIF' | 'VIDEO'
payload: any
}

type MessagePayload = {
text: string
}

type GifPayload = {
url: string
}

type VideoPayload = {
url: string
}


function handleMessage(message: Message) {
if (message.type === 'MESSAGE') {
const payload = message.payload as MessagePayload;
}
if (message.type === 'GIF') {
const payload = message.payload as GifPayload;
// do something
}
if (message.type === 'VIDEO') {
const payload = message.payload as VideoPayload;
// do something
}
}

Before i knew about discriminated unions i would spend time “wrangling” my data and casting it to get it to operate properly. While this works, it’s not optimal and becomes very error prone since you manually have to cast the signature every time you handle a message.

With

type Message = {
type: 'MESSAGE'
text: string
} | {
type: 'GIF'
url: string
} | {
type: 'VIDEO'
url: string
}



function handleMessage(message: Message) {
if (message.type === 'MESSAGE') {
const payload = message.text;
}
if (message.type === 'GIF') {
const payload = message.url;
// do something
}
if (message.type === 'VIDEO') {
const payload = message.url;
// do something
}
}

Now we’re getting somewhere. What happens is that once you’ve checked that the message type is one of the three we have defined here. It will “discriminate” and select the right signature. It provides many benefits. Firstly it will reduce accidental errors, since you’re not casting anything. Secondly you can avoid defining obscurely named variables like “payload”, because the inference will work out of the box.

Asserts

Have you ever felt annoyed when typescript complains that the variable may be undefined? Then you end up having to check the validity everywhere. There’s a better, but not that well known approach to this. The asserts keyword. In short, it allows you to predicate the type of a variable that is not within the functions scope. Let’s look at an example.

Okay… let me do an early return

It works now, but there is a better way.

We’ve now predicated that the profile is defined. I like to think of it as a point in a timeline, where the signature of the type changes.

In this example the benefits are perhaps not that apparent, but you can imagine how useful this will be in a more complex applications.

The intended behavior becomes much clearer. Read more about it here https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates

Overriding the global namespace

When building web apps it’s often that you have to integrate some external library that may be poorly typed. This can be a hassle especially working on properties defined on t+he window object. Let’s go through an example with a fake events library

type Library = {
trackEvent(event: string, data: any): void
}


const Lib = (window as any).EventLib as Library;

You may do something like this to get it to work. While this is an ok-ish approach, it introduces some annoyances. Like having to do this anywhere you want to use the library. You could define it as variable somewhere and export it, but this often introduces issues with SSR libraries such as Nextjs.

Here is the solution.

This merges the default Window interface and your custom types, so that it’s included in the global scope. This is called declaration merging. Read more about that here https://www.typescriptlang.org/docs/handbook/declaration-merging.html

--

--