Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using Branded types in Typescript #10

Open
heyitsaamir opened this issue Mar 27, 2024 · 0 comments
Open

Using Branded types in Typescript #10

heyitsaamir opened this issue Mar 27, 2024 · 0 comments

Comments

@heyitsaamir
Copy link
Owner


excerpt: 'Adding additional type-safety to distinguish same data-types'
date: '2023-01-01T00:29:32.431Z'

Javascript is inherently not a type-safe language. You don't get compile-time errors when you are doing potentially unsafe operations between incompatible types. Typescript has become a defacto way of improving javascript's shortcomings when it comes to type-safety and has actually made javascript a viable language to build large scale applications.

Let's take an example:

const foo = 5;
const bar = 'five';

...

if (foo !== bar) {
	console.log('foo is not bar');
} else {
	console.log('foo is bar');
}

With pure javascript, this piece of code compiles, and logs foo is not bar as expected. However, in any non trivial piece of code, the compiler should tell us that this operation is moot, and we might be breaking some assumptions. Let's update thie code with typescript:

const foo: number = 5;
const bar: string = 'five';

...

if (foo !== bar) {
	^^^^^^^^^^^ "This comparison appears to be unintentional because the types 'number' and 'string' have no overlap."
	console.log('foo is not bar');
} else {
	console.log('foo is bar');
}

Here typescript gives us a nice helpful message saying, hey you might be doing something wrong comparing two different types.
But what happens when two variables are of the same data type:

function convertToDate(isoDateString: string): Date {
	return new Date(isoString);
}

In this example, convertToDate takes in an isoString and converts it to a date object. This is an extremely common operation in javascript. Many APIs will return json results with iso dates typed as strings and it's the responsibility of the client to convert the value to a Date object if the client needs to use it as a date. The convertToDate function here takes any string as an input which can lead to a false sense of security that the code we're writing is safe. Take this code for example:

const apiDateString: string = '2023-01-01T00:29:32.431Z';

let convertedDate = convertToDate(apiDateString); // okay!

...

const userInputDateString = 'January 1st, 2023';

convertedDate =  convertToDate(poorlyNamedVariable); // type-safe but invalid

Here, our code doesn't provide any type-safety that userInputDateString is in fact an isoDate. It's a string so javascript and typescript are both happy. As you can imagine in a large scale application, if a developer is unaware of how convertToDate works, they may feel the need to perform validation before calling the function to be sure that they are supplying the right type of input. Or if the function throws, then they would need to handle a validation error everywhere that this function is called.

Is it possible for us to differentiate the types of apiDateString and userInputDateString such that we get type-safety? This is where brand types come in. A brand type adds another layer of safety to distinguish vales of the same data type. If we modify the above to something like this:

function convertToDate(isoDateString: ISODateString): Date {
	return new Date(isoDateString);
}

const apiDateString = '2023-01-01T00:29:32.431Z' as ISODateString;

let convertedDate = convertToDate(apiDateString); // okay!

const userInputDateString = 'January 1st, 2023';

convertedDate = convertToDate(userInputDateString);
                           // ^^^^^^^^^^^^^^^^^^^
                           // Type 'string' is not assignable to type '{ _brand: "ISODateString"; }'

If we could create completely new type safe data types, the above would give us a compile time error. The convertToDate function only accepts ISODateString types. If you (or an api) is providing some guarantee that the value being supplied is an ISODateString type, then the code will compile. Otherwise, it'll throw a compile-time error. This is possible with typescript with branded types. Here is what a generic Brand type looks like for string types:

export type BrandedString<TName extends string> = string & { _brand: TName };

For an ISODateString, you may get:

export type ISODateString = BrandedString<'ISODateString'>;

If a variable is assigned to an ISODateString type then, it's safe to be sent to convertToDate. Of course, you can use narrowing and type-predicates to perform validation from a regular string to assert that the string is in fact a branded string. Once that validation is done, however, we can be confident that the value being passed around is a true ISODateString.

const isISODateString = (input: string): input is ISODateString {
  // Really naive way to do validation, lol
	return !isNaN((new Date(input)).getTime());
}

if (isISODateString(userInputDateString)) {
	const date = convertToDate(userInputDateString) // okay!
}

We use this technique across the codebase at my company. We use it for:

  1. entity ids (there is no point in comparing UserId and CompanyId since they are always a disjointed set).
  2. data that has a unique format for data transmission (like date strings)
  3. metrics (weight metrics should never really be compared against spatial metrics)

In fact, as I was building out the bookmarks page, the API (I use Raindrop.Io) returned a format that resembled fields that contained some of the above types of data. I had to build a type for the json anyway, so my type looks something like this:

export type BookmarkId = BrandedNumber<'BookmarkId'>
export type ISODateString = BrandedString<'ISODateString'>;
export type Url = BrandedString<'Url'>;

export interface Bookmark {
	note: string
	_id: BookmarkId
	title: string
	link: Url
	created: ISODate
	lastUpdate: ISODate
}

Now we can have additional typesafety with the created, lastUpdate, link and _id fields. Here is a typescript playground with the above concepts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant