Skip navigation and jump to content

Felix Nehrke

I write about software development and software architecture.

deutsch

Branded Types

Typescript is basically just Javascript with slightly stricter rules and powerful types. This simple addition is actually surprisingly beneficial and allows us to have powerful intelli-sense in our editors and IDE’s. Typescript also gives us quick feedback when we have made mistakes and cause type conflicts. It’s no surprise that many developers like to use it and enjoy its features. But how do we actually use types and what can we do better? Let me introduce branded types to further improve our developments and get the most of it.

What are branded types

In short, branded types help avoid ambiguous parameter. Or, in other words, they help us distinguish between values of the same primitive type. For example, for a function with multiple parameters of the same primitive type (e.g. string), we can make the parameter order unique. This process is sometimes referred to as type-tagging, too.

Since we always use the correct type through branded types, we can avoid errors that are difficult to find. We also use them to increase the expressiveness of our code. In essence, they brand a parameter to a very specific use case and hide it’s underlying primitiv type from us. So we can enjoy the certainty that it’s very unlikely that we mess up our parameter-order and type-definitions in general.

The big improvement over other solutions for strict types is their ability to compile to the primitiv type. So they don’t pollute our final artifact with unnecessary fragments of code.

An example to grab the idea

Imagine you have a function which accepts a user-id and a group-id, and both of them are of type string. This function may look like this:

function canAccess(groupId: string, userId: string): boolean {
  // implementation
}

In this case the naming of the parameters make it obvious which parameter stands for the user-id and which represents the group-id. However, the compiler doesn’t prevent us from invoke the function with parameters in the wrong order. This means both call of the following example will compile an no warning will be thrown.

const userId = "123";
const grouId = "abc";
const allow1 = canAccess(groupId, userId); // correct order
const allow2 = canAccess(userId, groupId); // wrong order, no error
Note that some IDEs may show a hint, indicating that the order is incorrect. However, this doesn’t apply to all editors, and we cannot rely on it.

Introducing dedicated classes

The first approach which may come to my mind are classes to represent the types and to avoid this annoying circumstance. This is already a proven practise in many other languages such as Java, especially to restrict the types from certain cases and avoid errors. So let’s create a class called UserId and another one called GroupId to distinguish the ids.

class UserId {
  constructor(public userId: string) {}
}

class GroupId {
  constructor(public groupId: string) {}
}

Our actual code changes a bit, but we get an error, telling us that we made a mistake regarding the parameters.

// note the class initialisation `new ...`
const userId = new UserId("123");
const groupId = new GroupId("abc");
const allow1 = canAccess(groupId, userId); // correct order
const allow2 = canAccess(userId, groupId); // wrong order, error

The actual error-message is rather descriptive and explains in detail what went wrong. Unfortunately it further reveals a small, but important implementation detail, which we have to address as well.

Argument of type 'UserId' is not assignable to parameter of type 'GroupId'.
  Property 'groupId' is missing in type 'UserId' but required in type 'GroupId'.

The only reason we are getting the error here is because of the different constructor parameters userId and groupId. In fact, we don’t get an error if both are named id. It’s very easy to make this mistake and that’s a problem!

Beside of this detail we were able to solve our problem, but unfortunately it has a few more questionable consequences.

  1. how to serialize and de-serialize these classes correctly? e.g. for http-requests

  2. we have a lot of code now only to ensure type-safety

  3. even our compiled artifact has this code in it since it’s impossible for the compiler to find out if the code is necessary

Even though we may accept the later two problems and ignore them we still face the first problem. In consequence this is not really a viable solution, so we need to investigate deeper.

Abstract the idea to types

Since the classes don’t work out for our problem we might take a step back. Let’s define the types directly to represent our domain-types. This looks easy, since typescript has first class support for types!

type UserId = string;
type GroupId = string;

Indeed, it looks great, but it’s not a solution, unfortunately. The typescript-compiler eagerly tries to resolve types to their primitive base-types, and we only redefined the type string here. Therefore it’ll resolve our new shiny types to string, and we don’t win anything – except for way better documentation, yeah!

Make the type distinct from its primitive base

So our problem is that the compiler tries to resolve types to their primitive bases. However, what happens if we declare our type as non-primitive instead? Let’s try this and add a static value to our type that changes the type but nothing else.

type UserId = string & { __brand: 'UserId' };
type GroupId = string & { __brand: 'GroupId' };

This approach looks promising, and will in fact work, since our ids are not bare strings anymore. On the other hand this means we have to tell the compiler explicitly the types of our ids, now.

Explanation: we are expanding the string to include another attribute __brand, which is 'UserId' or 'GroupId' by default! This means that the strings are suddenly different types because the value of the static attribute is different!
// note the 'as UserId' and 'as GroupId'
const userId = "123" as UserId;
const groupId = "abc" as GroupId;
const allow1 = canAccess(groupId, userId); // correct order
const allow2 = canAccess(userId, groupId); // wrong order, error

The error we get from typescript is descriptive. It might be a bit to detailed with its explanation though, but it doesn’t reveal any secret implementation:

Argument of type 'UserId' is not assignable to parameter of type 'GroupId'.
  Type 'UserId' is not assignable to type '{ __brand: "GroupId"; }'.
    Types of property '__brand' are incompatible.
      Type '"UserId"' is not assignable to type '"GroupId"'.

The really great thing about this approach is its versatility. We’d not only created a valid type-distinction but also made sure that we avoid the downsides from the first approach. When we serialize or de-serialize these values as JSON or use them in console.log(), they are actually treated like strings. Furthermore, we don’t pollute the compiled artifact with any obsolete code. We only juggle with types and these are removed during compilation!

Improve to a general branded type

Now that we understand branded types let’s abstract the concept a bit more. When we take a look at the type-definition we can easily spot some duplication (& { __brand: something }). This is bad because it’s not only duplicated code, but it doesn’t tell a story why it’s there either.

Let’s solve these issues by introducing a helper type to brand types. Such a type allow us to hide the implementation details and improves the readability of our code better! Furthermore, we can improve the branding itself a bit by marking the __brand property readonly and make it a unique Symbol.

declare const __brand: unique symbol;
type Branded<T, K> = T & { readonly [__brand]: K }

type UserId = Branded<string, 'UserId'>;
type GroupId = Branded<string, 'GroupId'>;

const userId = "123" as UserId;
const groupId = "abc" as GroupId;

Final thoughts

We’ve seen how easily we can improve the type safety of our code by using branded types. The examples shown are just the tip of the iceberg. I’m not just thinking about IDs, but also about various unit systems, e.g. B. Currencies or metrics. Never swap EUR and USD, or meters and yards again. It would also be conceivable to mark API returns with branding, for example to distinguish between successful and unsuccessful returns.

In essence, I think at least the knowledge about such a powerful addition have to belong to the toolbox of every typescript developer. So I hope I spread some inspiration, and you’ve added the shown type to your utilities.d.ts already.

Happy typing.