Avoid stupid mistakes with type Branding and Flavoring
Tags: typescript, node, programming, zod
Published: 2024-04-07 05:49:12

In a structural type system like TypeScript, functions that accept multiple arguments of the same type can sometimes lead to unexpected behavior. This is because TypeScript only considers the structure of the arguments, not their explicit type names, when determining type compatibility.

Here's an example that illustrates this potential issue:
```typescript
type BlogId = string;
type PostId = string;

async function getPost(blogId: BlogId, postId: PostId) {
  return posts.findOne({ blogId, postId });
}

// this code line compiles, but doesn't work correctly
const result = await getPost(postId, blogId);
```
# What can we do about it?

Thanks to [David](https://x.com/davidshimon), a colleague of mine, I've recently learned about two new techniques to avoid this problem - **Branding** and **Flavoring**.

## Branding

Branding in TypeScript is a technique that allows you to create distinct types from any existing type, whether it's a primitive type (like string, number, or boolean) or a complex type (like an object, interface, class, or function). Branding is a form of nominal typing, which contrasts with TypeScript's default structural typing behavior.

With branding, you can introduce [nominal typing](https://en.wikipedia.org/wiki/Nominal_type_system) for specific types in your codebase, ensuring that values representing different concepts or entities cannot be mixed accidentally, even if they share the same underlying structure or primitive type.

Let's see how this is done with our previous example:
```typescript
// Branding a string
type PostId = string & { __brand: 'PostId' };
type BlogId = string & { __brand: 'BlogId' };

const postId: PostId = 'post-123' as PostId;
const blogId: BlogId = 'blog-456' as BlogId;

async function getPost(blogId: BlogId, postId: PostId) {
  return posts.findOne({ blogId, postId });
}

// Doesn't compile
const result = await getPost(postId, blogId);
```
In this example, we brand the `string` type by using an intersection type with an object literal that has a special `__brand` property. `PostId` and `BrandId` are distinct branded types, even though their underlying primitive type is `string`.

If we are using an input validation library like [Zod](zod.dev), we can avoid casting when branding values:

```typescript
const PostId = z.string().brand('PostId');
const BlogId = z.string().brand('BlogId');
// Branding a string
type PostId = z.infer<typeof PostId>
type BlogId = z.infer<typeof BlogId>

const postId: PostId = PostId.parse('post-123');
const blogId: BlogId = BlogId.parse('blog-456');
```

_(checkout [my blog posts series on Zod](https://dev.to/shaharke/zod-zero-to-hero-chapter-1-5bna) if you are not familiar with it)_

While branding is useful for preventing careless mistakes, such as passing an incorrect value to the wrong positional argument of a function, it is not a foolproof solution It merely shifts the responsibility of properly constructing and branding values to the point where they are created. We can still make mistakes at that stage and inadvertently brand an incorrect value.

## The problem with Branding

Branding has one caveat: we can no longer assign values to variables directly without 'blessing' them, either through casting or using a library like Zod. This can be problematic when writing tests or working with auto-generated code. For example:

```typescript
type PostId = string & { __brand: 'PostId' };
const postId = "abc"; // This code doesn't compile
```

This issue is thoroughly explained in [this great article](https://spin.atomicobject.com/typescript-flexible-nominal-typing/) and I highly recommend reading it. In the article, we are introduced to the *Flavor* solution. In short, flavoring is similar to branding but allows implicit conversion, addressing the issue we discussed earlier:

```typescript
type Flavoring<FlavorT> {
  _type?: FlavorT;
}
type Flavor<T, FlavorT> = T & Flavoring<FlavorT>;
type PostId = Flavor<string, 'PostId'>;
const postId: PostId = "abc"; // This code compiles

type BlogId = Flavor<string, 'BlogId'>;
const blogId: BlogId = postId; // But this still doesn't
```

Why this works is perfectly explained in the original article, so I'll just quote it:

> TypeScript won’t let us mix our ID types because, under the hood, the `_type` properties are incompatible. The optional string "PostId" is not assignable to the optional string "BlogId". But because the `_type` is optional, implicit conversion from unflavored values of the same underlying type is allowed. Once implicit conversion happens, however, TypeScript will henceforth treat the value as potentially having the declared `_type` and disallow mixing with other, differently flavored types. Thus, downstream consumers of the value will get the safety and semantic benefits.

## Using Flavor with Zod

An immediate question arises: can we use Flavors with Zod? When using Zod, we first define a schema, and then infer types based on that schema. How can we use the `Flavor` type we wrote above as part of our Zod schemas? 

To achieve that, we need to understand refining in Zod. Zod's refine method allows you to define a custom validation function for a schema, enabling you to enforce more complex validation rules beyond the built-in validations provided by Zod.

Here's a basic example of using Zod's `refine` method:
```typescript
import { z } from 'zod';

// Define a schema for a user's age
const ageSchema = z.number().refine(
  (age) => age >= 18,
  { message: 'User must be at least 18 years old' }
);

// Usage
try {
  const validAge = ageSchema.parse(20); // Valid age
  console.log(validAge); // Output: 20

  const invalidAge = ageSchema.parse(16); // Invalid age
} catch (error) {
  console.error(error.message); // Output: User must be at least 18 years old
}
```

The first argument of the `refine` method is a function that accepts a single argument, the validated input - a `number` in this case, and returns a `boolean`. Interestingly, if we pass a [Type Guard function](https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards), the parsed input type is narrowed based on the type predicate defined for the type guard.

```typescript
type BlogId = Flavor<string, 'BlogId'>;
function isBlogId(id: string): id is BlogId {
  return !!id;
}
const BlogIdSchema = z.string().refine(isBlogId);
// The type of value is BlogId and not string
const value = BlogIdSchema.parse("123"); 
```

## Summary

TypeScript is a structural type system, meaning it doesn't prevent us from passing an incorrect value to a function if the value's type matches the function parameter's type. A common technique to address this issue is Branding, which distinguishes between values with the same structure. While branding is useful, it makes value construction somewhat cumbersome. Flavoring is a more advanced approach that allows implicit conversion for easily constructing values while maintaining type safety. Zod provides built-in support for Branding. For Flavoring, we need to leverage type guard functions and Zod's refine method.