Using Algebraic Data Types (ADTs) in TypeScript
Tags: typescript, adts, javascript
Published: 2023-03-16 17:58:06

Algebraic Data Types (ADTs) are concepts originally from functional programming languages, representing composite types formed by combining other types. In TypeScript, ADTs can be achieved using union types ("|" operator), intersection types ("&" operator), and discriminated unions.

```TypeScript
type Alien = {
  type: 'unknown';
}

type Animal = {
  species: string;
  type: 'animal';
}

type Human = Omit<Animal, 'type'> & {
  name: string;
  type: 'human';
}

type User = Animal | Human;

```

A discriminated union in TypeScript is a way to create a type that combines union types and has a common property that can be used to determine the specific type of the variable at runtime. In the example above, **type: 'animal' or 'human'** is the common property.

Algebraic Data Types (ADTs) provide several benefits:

### Expressiveness and Maintainability:

ADTs allow to model complex data structures and relationships naturally by providing clear, self-documenting type definitions. They enable you to create composite types that accurately represent the domain, making the code easier to understand and reason about.

### Type safety: 
By using ADTs, you can explicitly define the structure of your data, making it easier for the TypeScript compiler to catch potential issues at compile time.

### Pattern matching and exhaustiveness checks: 
When working with ADTs, especially sum types (discriminated unions), you can leverage TypeScript's ability to perform pattern matching and exhaustiveness checks. These checks help ensure that you've handled all possible cases when working with a value, reducing the chances of bugs due to unhandled cases.

### Encapsulation of logic: 
ADTs can help you encapsulate the logic related to specific data structures, such as validation or transformation, making your code more modular and easier to reason about.

## Utility Types

TypeScript provides several [utility types](https://www.typescriptlang.org/docs/handbook/utility-types.html) to help describing the composition such like the ADTs.

```TypeScript
type Human = Omit<Animal, 'type'> & {
  name: string;
  type: 'human';
}
```

- **Omit<Animal, 'type'>** : 
This is a utility type that creates a new type by removing the specified keys from the given type. In this case, it removes the type property from the Animal type.
- **&: {  }** : 
The intersection type operator combines two or more types into a single type that has all the properties of the original types.

By combining the types with the intersection type operator, the Human type ends up having the properties species and name, as well as the discriminant type with the value 'human'. The Human type is essentially an extension of the Animal type with some modifications.

Finally, User is a union type that can represent any of the Animal, Human, or Adult types.

### Difference between **extends** and **& intersection**

It is possible to write the similar data structure by using interface with **extends**. 

```TypeScript
interface Animal {
  species: string;
  type: 'animal';
}

interface Human extends Omit<Animal, 'type'> {
  name: string;
  type: 'human';
}
```

The **interface** keyword is primarily used for defining object types, while the **type** keyword is used for defining any type, including object types, union types, and intersection types.

One key difference between using **extends** and **&** is that **extends** creates a new type that has an inheritance relationship with the parent type, while **&** creates a new type that is a combination of two or more existing types without any inheritance relationship.

It is a personal preference, but **type** could be more flexible when you have to describe a lot of intersections.


## How to predict unknown data

Suppose we have the following un-typed data, how can we let TypeScript compile to allocate them to the proper types?

```TypeScript
const ghost: unknown = null;
const alien: unknown = {}

const sheep: unknown = {
  species: 'sheep',
}

const jun: unknown = {
  name: 'Jun',
  species: 'human',
}

const anomany: unknown = {
  name: 'anomany',
}
```

## User-defined type guards

User-defined type guards are a feature in TypeScript that allows you to create custom type-checking functions that can narrow down the type of a value within a specific scope.

- Takes a value as a parameter, which is usually of a more general type, such as unknown or a union type.
- Returns a boolean value, which indicates whether the value matches the expected type.
- Uses a special **type predicate** in the return type annotation, in the format "**value is SpecificType**". This tells TypeScript that the function can narrow down the type of the value to SpecificType when it returns true. In the following example, "someone is Animal", "someone is Human" and "data is { [key: string]: unknown }" are the **type predicates** 

```TypeScript
function hasProperty(data: unknown): data is { [key: string]: unknown } {
  return data != null;
}

function isAnimal(someone: unknown): someone is Animal {
  return hasProperty(someone) && typeof someone.species === 'string'
}

function isHuman(someone: unknown): someone is Human {
  return hasProperty(someone)
  && typeof someone.name === 'string'
  && someone.species === 'human';
}
```

**hasProperty** is a type guard function that checks if data is an object with string keys and unknown values. 

**isAnimal** and **isHuman** are type guard functions for the Animal and Human types, respectively. These functions check whether an unknown entity has the required properties and correct types to be an Animal or Human.


```TypeScript
function categorise(item: unknown): User | Alien {
  if (isHuman(item)) {
    return { ...item, type: 'human' } as Human;
  }
  if (isAnimal(item)) {
    return { ...item, type: 'animal' } as Animal;
  }
  return { type: 'unknown' } as Alien;
}
```

The categorise function takes an unknown item and returns a User (either Animal or Human) or an Alien. It uses the type guard functions isHuman and isAnimal to check the type of the item and adds the type property accordingly.

```TypeScript
function parseItems(items: unknown[]): User[] {
  return items.flatMap((item) => {
    const categorised = categorise(item)
    return categorised.type === 'unknown' ? [] : [categorised];
  })
}
```

The parseItems function takes an array of unknown items and returns an array of User entities. It categorises each item using the categorise function and filters out any items with the type property set to 'unknown'.

## Render data according to the type

Once each data is assigned to a specific type, you can safely consume the data. For example, if you want to render them differently according to the type:

```TypeScript
function renderHuman(someone: Human) {
  return `${someone.name} is a human`;
}

function renderAnimal(someone: Animal) {
  return `It is a ${someone.species}`;
}

function renderItems(items: User[]) {
  return items.map((item) => {
    switch (item.type) {
      case 'human':
        return renderHuman(item);
      case 'animal':
        return renderAnimal(item);
      default:
        return 'unknown';
    }
  });
}
```
Your editor can show code suggestions and errors benefitting the type safety.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0mdekbxageqxiq63bjr4.png)

And the previous data will render the expected output.

```TypeScript
const list = parseItems([ghost, alien, anomany, jun, sheep]);
console.log(renderItems(list))

[ 'Jun is a human', 'It is a sheep' ]
```