Domain Modeling with Union Types in Typescript
Tags: typescript, adt, javascript
Published: 2020-05-03 09:57:55


Typescript is one of the most interesting languages of the decade,
this language is a superset of javascript that has the ability to provide type definition to your javascript codebase.

Union types are one of the type definitions you might encounter in typescript, we will show how does typescript infers union type, and also explore examples of how union type is used in domain modeling. 

I do not claim that I'm an expert in domain modeling. It is a wide field on its own right, but with that caveat in mind lets go and explore anyway.


# The need for Union Types

Typescript has to deal with existing javascript annotating it with a type.

Consider a simple function in javascript:

```javascript
    function getAge (name) {

        if (name === "jon") {
            return "20"
        } else {
            return 14
        }
    }
```

When we switch to typescript, Typescript has to provide a return type definition of the function **getAge**.

The return type would be. **string | number**, thus typescript would use union type to express that this function returns either a **string** or a **number**, or more explicitly **"20" | 14**. This allows typescript to transition from javascript and providing inferred type definition.

If we use this in another context, let's say foo, we will get an error.

```typescript
    function compute(args: []) {
        ...
    }
    
    // Error. Type string | number is not assignable to []
    isLegalAge(getAge("sansa"))
```


# Extending the Domain with Union Type

In a real-world scenario, let's say we have a type **Person** with **firstname** and **lastname**, extend this information to provide an **age**.

```typescript
    type Person = {
        firstname: string
        lastname: string
    }
   
    let jonSnow: Person = { firstname: 'jon', lastname: 'snow' }

    // Error: `age` does not exist in type Person.
    jonSnow = { ...jonSnow, age: 20 }
```

for some of us might think,

> Just add the age and make it optional.

```typescript
    type Person = {
        firstname: string
        lastname: string
        age?: number // added optional age.
    }
```

This would work, but our new type definition it doesn't tell much everything,
is the age really optional information of a person? What we want is to provide an interface that strongly consists of information we need, perhaps clients of this type will implement like this.

```typescript
    type Person = {
        firstname: string
        lastname: string
        age?: number // added optional age.
    }

    function isLegalAge (person: Person): boolean {

        // ensure the age is present in the Person structure.
        if (!person.age) {

            // person age does not exist? what should I return?
            return false // maybe just false?.
        } else { // else proceed to real computation.

            // compute legal age.
            return person.age >= 18
        }
    }
```

This not guarantee that the clients of our type a strong definition.
The client will have to wonder why the age is optional, also this does not tell more information about our type, questions might arise:

- On our domain, we might ask. Is our domain have an age restriction?
- On the technical side, we might ask. If the person doesn't have age, do we return **false** on our **isLegalAge** function?

> Well let's add another field in type Person.

This would work as well, after all, we have declared the type in our own setup.
However, consider the type **Person** to be in private, like in most of the APIs? or you have implemented this library and don't want your users to force implementing your new interface?


# Union type to the rescue

The problem we're facing is that we would like to retain the old type implementation but we want to extend the type annotation, one solution is to implement a union type.

first let's create a new type annotation to express our intent.
```typescript
    type PersonWithAge = {
        firstname: string
        lastname: string
        age: number
    }
```

and general union type information, we have.

```typescript
    type PersonInfo =
        | Person
        | PersonWithAge
```
Now, our domain says, **PersonInfo** is either **Person (legacy information) or PersonWithAge**.

And from previous our code we could use, **PersonInfo** instead of **Person**.

```typescript
    let jonSnow: PersonInfo = { firstname: 'jon', lastname: 'snow' }

    jonSnow = { ...jonSnow, age: 20 } // ok
```

or we could create another person with age information without breaking the legacy code.

```typescript
    let jonSnow: Person = { firstname: 'jon', lastname: 'snow' }

    let jonSnowWithAge: PersonWithAge = { ...jonSnow, age: 20 }
```

We could also create a formatter that accepts our newly created union type.

```typescript
    function formatPersonName (personInfo: PersonInfo): string {
        return personInfo.lastname + ", " + personInfo.firstname;
    }

    // Handled legacy code.
    formatPersonName(jonSnow)

    formatPersonName(jonSnowWithAge)
```

and from our **isLegal** function, we could strictly use **PersonWithAge** only

```typescript
    function isLegalAge (person: PersonWithAge): boolean {
        return person.age >= 18
    }
```

does eliminate the need to check the age, and has much cleaner code.


# Conclusion

Union types are great for exposing our domain, we could add flexibility to our system without breaking the previous legacy domain, It also provides a self-documenting type definition and what would be the shape of our data.

Just a small warning, you find yourself the need to provide a case analysis of which type does it belong, there are many techniques available. One of the most used is tagged union.

Union type is not the only way to extend our domain in typescript, typescript also provides an intersection type.

Let me know your thoughts below, happy coding.