Typescript Tip: Safer functions with branded types.
Anderson. J
Posted on April 11, 2022
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 []
}
getPosts
accepts a page
argument and retrieves posts from a data source.
Now, what if getPosts
is used like this?
const posts = getPosts(-1);
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')`
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;
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' };
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 []
}
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'.
Let's try with a valid number:
const posts = getPosts(2);
// ^^^ Argument of type 'number' is not assignable to parameter of type 'PositiveNumber'.
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);
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);
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');
}
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
If you liked this tip, please, share it and hit the β₯
Further Reading:
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
November 28, 2024
October 16, 2024