Typescript Tip: Safer functions with branded types.

andersonjoseph

Anderson. J

Posted on April 11, 2022

Typescript Tip: Safer functions with branded types.

Imagine you have a function like this one:

interface Post {
  id: number
  title: string
}

export function getPosts(page: number): Post[] {
  // perform some query to return posts from a database
  // ...
  // ...
  return []
}
Enter fullscreen mode Exit fullscreen mode

getPosts accepts a page argument and retrieves posts from a data source.

Now, what if getPosts is used like this?

const posts = getPosts(-1);
Enter fullscreen mode Exit fullscreen mode

Obviously That's incorrect because a page number can not be negative. A simple solution would be add a line like this one:

if(page < 0) throw Error('Page must be a positive number')`
Enter fullscreen mode Exit fullscreen mode

But would not be nice if we receive a type error when we try to call the function with a negative number?

The problem here, is that the type number is too general for our function. number can be any number and we are expecting just a positive number.

We can create a type alias for it:

type PositiveNumber = number;
Enter fullscreen mode Exit fullscreen mode

Although it makes our code more readable, using a simple alias doesn't solve the problem. Because we are still able to pass any number as an argument to getPosts().

This is when we can create a Branded Type.

Branded Types

When the base type is too general we can use this pattern.

type PositiveNumber = number & { __type: 'PositiveNumber' };
Enter fullscreen mode Exit fullscreen mode

Here we are using an intersection between the base type (number) and an object with the __type property, this private property holds the brand type, and its used to differentiate it from a regular number.

We can rewrite our function to:

type PositiveNumber = number & { __type: 'PositiveNumber' };

export function getPosts(page: PositiveNumber): Post[] {
  // ...
  return []
}
Enter fullscreen mode Exit fullscreen mode

Now if we try to call it like before, we're going to receive the error we expect.

const posts = getPosts(-1); 
// ^^^ Argument of type 'number' is not assignable to parameter of type 'PositiveNumber'.
Enter fullscreen mode Exit fullscreen mode

Let's try with a valid number:

const posts = getPosts(2); 
// ^^^ Argument of type 'number' is not assignable to parameter of type 'PositiveNumber'.
Enter fullscreen mode Exit fullscreen mode

Hmm, that's doesn't make sense πŸ€”, I'm sure 2 is a positive number.

We are still receiving the same error because we are still passing a number, not a PositiveNumber. Son wee need to explicitly say to the compiler that we are passing a PositiveNumber using the as keyword.

const posts = getPosts(2 as PositiveNumber); 
Enter fullscreen mode Exit fullscreen mode

That will get rid of the error.

Now, what if we have a variable that we can't tell if it's a positive number or not?

const { page } = incomingRequest.body;  
// here, page could be a positive or a negative number, so we can't use the `as` keyword because we can't be sure of what is the actual value;

const posts = getPosts(page); 
Enter fullscreen mode Exit fullscreen mode

To solve this we can use an assertion function that asserts page is an actual PositiveNumber

function assertsPositiveNumber(value: number): asserts value is PositiveNumber {
  if(value < 0) throw new Error('Value must be a positive number');
}
Enter fullscreen mode Exit fullscreen mode

Now with this assertion, we can tell if page is a positive number or not.

const { page } = incomingRequest.body;  

assertsPositiveNumber(page);

const posts = getPosts(page); // at this point TypeScript know that `page` its a PositiveNumber
Enter fullscreen mode Exit fullscreen mode

If you liked this tip, please, share it and hit the β™₯

Further Reading:

πŸ’– πŸ’ͺ πŸ™… 🚩
andersonjoseph
Anderson. J

Posted on April 11, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related