What are TypeScript Discriminated Unions?
Tags: typescript, programming, javascript, webdev
Published: 2023-07-19 19:54:27

A discriminated union is a TypeScript feature that enables the creation of a type that can represent several different possibilities or variants. By attaching discriminators to each variant, TypeScript's type system can help ensure that we handle all possible cases gracefully. Discriminators can be string literals, numeric literals, or even symbols.

## Why are Discriminated Unions important? 

Using discriminated unions in your TypeScript code brings numerous benefits: 

1. Improved Type Safety: With discriminated unions, TypeScript can ensure that all possible variants of a type are accounted for, eliminating the risk of undefined or unexpected behavior at runtime. 
2. Enhanced Autocompletion: IDEs and code editors can leverage the discriminators to provide accurate autocompletion suggestions based on the specific variant being handled. 
3. Better Code Maintainability: Discriminated unions make code easier to read and understand by explicitly indicating the possible cases for a given type. 
4. Comprehensive Error Handling: TypeScript's static type checking can help us catch missing or mismatched discriminators, reducing the likelihood of introducing bugs.

## But how it works?

Let's build a simple example to understand how it works.

We have a type or interface called Vehicle, which looks something like this:

```typescript
type Vehicle = {
  type: 'motorbike' | 'car';
  make: string;
  model: string;
  fuel: 'petrol' | 'diesel',
  doors?: number;
  bootSize?: number;
}

const myCar: Vehicle = {
  make: 'vw',
  model: 'golf',
  fuel: 'diesel',
  type: 'car',
  bootSize: 400,
  doors: 5,
};
```

As you can see we have common properties, some that we expect to be always there for both Cars and Motorbikes, like `make`, `model`, `fuel` and a `type` property to discriminate what type of object we have, that could be either `motorbike` or `car`.
We also have some optional properties, like `doors`, which cars have, but not (most of) motorbikes, and a `bootSize`.

And we use that Type to use it for object called `myCar`.

### So what's the problem?

Let's say that now we have a new object, for our motorbike, and it looks something like this:

```typescript
const myBike: Vehicle = {
  type: 'motorbike',
  make: 'honda',
  model: 'cbr',
  fuel: 'petrol',
}
```

The problem here is that nobody is stopping us to do this instead:

```typescript
const myBike: Vehicle = {
  type: 'motorbike',
  make: 'honda',
  model: 'cbr',
  fuel: 'diesel',
  doors: 10,
  bootSize: 100,
}
```

Which it makes no sense, as normally a motorbike doesn't have doors, or a boot, and pretty sure they don't run on diesel either!
On the other side, also we made optional the `doors` and `bootSize` optional properties to accommodate the the motorbike type, but cars normally have doors and boots, so we want to have those properties compulsory if the **type is a car**!

How we can achieve that? Well with a discriminated union, where our discrimination field is our `type` property.

The first thing is to figure out what are the fields that will be in common for both types:

```typescript
type Vehicle = {
  make: string;
  model: string;
  fuel: 'petrol' | 'diesel',
}
```

Next, we use the discriminated union by extending the `Vehicle` type with the operator `&`, and using a union to extend it to two different types base on the `type`, so something like this:

```typescript
type Vehicle = {
  make: string;
  model: string;
  fuel: 'petrol' | 'diesel',
} & ({ type: 'car' } | { type: 'motorbike' });
```

At the moment is not adding much, but if we pass the `type: 'car'`, the type will be a union of the `Vehicle` type, and the `{ type: 'car' }` type, ignoring the other type in the union.

To make it more readable, we can also separate those types as follows:

```typescript
type MotorBike = {
  type: 'motorbike';
}

type Car = {
  type: 'car';
}
```

And rewrite the discriminated union as follows:

```typescript
type Vehicle = {
  make: string;
  model: string;
  fuel: 'petrol' | 'diesel',
} & (Car | MotorBike);
```

Now we can start to add the properties we want for motorbikes, for example we want to clarify that motorbikes will only have `petrol` as a fuel, and cars must have doors and boots:

```typescript
type MotorBike = {
  type: 'motorbike';
  fuel: 'petrol';
}

type Car = {
  type: 'car';
  doors: number;
  bootSize: number;
}
```

Which also means that if you create a new object as follows of the motorbike type, and you will pass `diesel` as a fuel, the IDE will throw you an error!

![Syntax Error TS](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ch150bptbp1yx0qsanua.png)

Vice-versa if you create a car object, and you don't pass the doors or the bootSize properties, you will get an error, while the motorbike will not!

```typescript
// This is valid
const myBike: Vehicle = {
  type: 'motorbike',
  make: 'honda',
  model: 'cbr',
  fuel: 'petrol',
}

// This is also valid
const myCar: Vehicle = {
  make: 'vw',
  model: 'golf',
  fuel: 'diesel',
  type: 'car',
  bootSize: 400,
  doors: 5,
};

// This will throw a TS error
const myCar: Vehicle = { // <- myCar will show an error
  make: 'vw',
  model: 'golf',
  fuel: 'diesel',
  type: 'car',
};

// This will also throw a TS Error as doors doesn't exists on that type
const myBike: Vehicle = {
  type: 'motorbike',
  make: 'honda',
  model: 'cbr',
  fuel: 'petrol',
  doors: 5, // <-- will error here
}
```

## Handling different properties with Type Guards

The next question you might ask yourself then is: "how do I handle these properties that might or might not exists as my object might have different shapes?".

The answer is: **Type Guards**.

### But what are Type Guards?

In TypeScript, a type guard is a feature that allows you to narrow down the type of a variable within a conditional block. It provides a way to perform type-specific operations and access properties or methods that are only available on certain types. Type guards are particularly useful when working with union types or discriminated unions.

Type guards are essentially runtime checks that inform the TypeScript compiler about the type of a variable. Once the type is narrowed down within the guarded block, the compiler can provide more accurate type checking and enable autocompletion for the specific type.

For example if we want to have a function that handles a vehicle, we might have something like this:

```typescript
const vehicleHandler = (vehicle: Vehicle) => {
  switch (vehicle.type) {
    case 'car':
      console.log(`The car has ${vehicle.doors} doors`);  
      break;
    case 'motorbike':
      console.log(`The only fuel my motorbike can take is ${vehicle.fuel}`);
      break;
  }
}
```

Essentially by using this switch statement, or an if condition, as long is a conditional block, the IDE will only show the properties available in that block, for example my IDE will show this in the `motorbike` condition:


![Type Guards](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8b2kcwpp7yjt8q5i7j0z.png)

Showing only the properties available for the appropriate type.

## Disclaimer

Parts of this article was created with the help of AI.