Article

Usecases for TypeScript Brand Types

Use TypeScript Brand Types to ensure type-safe data validation, automating checks at the type level. Improve code reliability and prevent implicit assumptions.

Imagine a situation where you ask the user to pass their email address...

It needs to be validated before processing it further.

Yet, the email is of type string. You store it here and there... and some time later, when you actually send the email, you're not 100% sure if the string is a valid email. So just in case, you validate it once again...

If you rely on the initial validation only, things will work correctly. Just until a new developer comes and introduces a change which break your implicit assumption about the code. They didn't know that whatever you keep within this string had to meet some criteria. It wasn't documented.

But more importantly, it wasn't automatically checked.

TypeScript Brand Types come to the rescue 🥳

The whole point is that you create a special subtype of a primitive type. Like a special string: email, skill. Or a special number: age, money, and so on:

type Email = Brand<string, "EMAIL">
type Skill = Brand<string, "SKILL">
type Age = Brand<number, "AGE">
type Money = Brand<number, "MONEY">

course Pretty straightforward, isn't it? 😊

You don't need to understand the internal declaration (although you will after watching the Domain Data Modeling using TypeScript Aliases, Brand Types and Value Objects course):

declare const __brand__type__: unique symbol;
type Brand<BaseType, BrandName> = BaseType & {
  readonly [__brand__type__]: BrandName;
};

The key is that each brand has a different shape. Therefore it makes them incompatible across different brands, e.g. you can't pass an ordinary string to where email is expected. Or you can't pass email to where skill is expected and so on:

declare const str: string
declare const email: Email
declare const skill: Skill

email = str // ❌ FAIL!
skill = email // ❌ FAIL!

So how does it make you benefit?

After a string is passed from the user, we don't know yet, whether it meets our conditions (it's a valid email, or whatever).

const stringFromUser: string = '...';

We run the validation, but we also leverage TypeScript's type system to make the type more precise:

function isValidEmail(maybeEmail: string): maybeEmail is Email {
  return emailValidation(maybeEmail) // assuming it returns a boolean
}

The compiler is now aware that the email is not just an ordinary string. It has more information:

const stringFromUser: string = '...';

if (isValidEmail(stringFromUser)){ // `string` type here
  stringFromUser // `Email` type here
}

And from now on, rest assured, whenever you get an expression of type Email, it has already passed the validation. And it is perfectly automated on the type-level.

Brand Types are a super neat way to adjust the type system to whatever your domain is.

Watch the Domain Data Modeling using TypeScript Aliases, Brand Types and Value Objects while I prepare more TypeScript tips!