DEV Community

RedCalmContemplator
RedCalmContemplator

Posted on

Domain Modeling with Union Types in Typescript

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:

    function getAge (name) {

        if (name === "jon") {
            return "20"
        } else {
            return 14
        }
    }
Enter fullscreen mode Exit fullscreen mode

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.

    function compute(args: []) {
        ...
    }

    // Error. Type string | number is not assignable to []
    isLegalAge(getAge("sansa"))
Enter fullscreen mode Exit fullscreen mode

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.

    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 }
Enter fullscreen mode Exit fullscreen mode

for some of us might think,

Just add the age and make it optional.

    type Person = {
        firstname: string
        lastname: string
        age?: number // added optional age.
    }
Enter fullscreen mode Exit fullscreen mode

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.

    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
        }
    }
Enter fullscreen mode Exit fullscreen mode

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.

    type PersonWithAge = {
        firstname: string
        lastname: string
        age: number
    }
Enter fullscreen mode Exit fullscreen mode

and general union type information, we have.

    type PersonInfo =
        | Person
        | PersonWithAge
Enter fullscreen mode Exit fullscreen mode

Now, our domain says, PersonInfo is either Person (legacy information) or PersonWithAge.

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

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

    jonSnow = { ...jonSnow, age: 20 } // ok
Enter fullscreen mode Exit fullscreen mode

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

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

    let jonSnowWithAge: PersonWithAge = { ...jonSnow, age: 20 }
Enter fullscreen mode Exit fullscreen mode

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

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

    // Handled legacy code.
    formatPersonName(jonSnow)

    formatPersonName(jonSnowWithAge)
Enter fullscreen mode Exit fullscreen mode

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

    function isLegalAge (person: PersonWithAge): boolean {
        return person.age >= 18
    }
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)