Back
Tech 3 min read - 22 Mar. 23 - Matthieu Locussol

Type Branding & Flavoring: Make your TypeScript code more readable and robust

Motivations

TypeScript's type system is structural and that's one of its main advantages. This characteristic offers numerous powerful tools to make invalid states unrepresentable, thus allowing potential bugs to be detected at compilation rather than at runtime.
However, this system is not always sufficient. For example, there are real-world use cases where it is desirable for two variables to be differentiated because they have a different type name, even if they have strictly the same structure.
Example:
type PostId = number;
type CommentId = number;

const postId: PostId = post.id;
const commentId: CommentId = postId; // OK
It is possible to assign to the variable commentId the content of the variable postId, after all, they are both number if we look at their type definition. However, do we really want this to be possible?
From a business perspective of our application, it doesn't really make sense. A blog post is not the same as a comment. That's good! This is exactly the type of business constraint that type branding allows us to implement.

Branding

Definition

The concept of branding consists of adding a distinctive field to our type to differentiate it from others. This field will only be useful to the TypeScript compiler to statically determine whether two types are compatible or not.
Here is our same example using type branding:
type PostId = number & { __brand: 'PostId' };
type CommentId = number & { __brand: 'CommentId' };

const value = 1 as PostId;

const postId: PostId = value; // OK
const commentId: CommentId = value; // Erreur
Although assigning a type of PostId to a type of CommentId poses no problem at runtime, it now generates a compilation error and prevents business errors.
It is common to define a generic type that allows branded types to be generated:
type Brand<T, U> = T & { __brand: U };

type PostId = Brand<number, 'PostId'>;
type CommentId = Brand<number, 'CommentId'>;
Note : A type such as Brand can exist because the intersection between number (here) and a JS object is allowed by JavaScript. In most other typed languages, such an intersection would be equivalent to "never" and would make no sense.

Limitations

  • Changing a type to a branded type requires manual casting;
  • It is possible to read the property __brand ;
  • No implicit conversion is possible, for example:
type Post = Brand<{ author: string; content: string; }, 'Post'>;

const createPost = (post: Post) => { ... };

createPost({ author: 'matthieu', content: 'Hello world!' }); // Erreur

Flavoring

Definition

Flavouring is similar in every respect to branding, with the sole difference that the property __brand is made optional. This technique allows us to have implicit conversion for types and objects:
type Flavor<T, U> = T & { __flavor?: U };

type Post = Flavor<{ author: string; content: string; }, 'Post'>;
type PostComment = Flavor<{ author: string; content: string; }, 'PostComment'>;

const createPost = (post: Post) => { ... };
createPost({ author: 'matthieu', content: 'Hello world!' }); // OK

const comment: PostComment = { author: 'matthieu', content: 'Hello world!' };
createPost(comment); // Erreur
Despite this, we note that we have still maintained incompatibility between different types with the same structure. This is also true for primitive types:
type Flavor<T, U> = T & { __flavor?: U };

type PostId = Flavor<number, 'PostId'>
type CommentId = Flavor<number, 'CommentId'>

const postId: PostId = 1;
const commentId: CommentId = postId; // Erreur

Limitations

  • It is still possible to read the property __flavor ;
  • Less strict than branding, implicit conversion can lead to errors and should be used wisely

Conclusion

While it is commonly accepted that branding is preferable for primitive types, flavouring can be preferable for objects to benefit from implicit conversion. A conditional type can be used to do this work for us:
type Brand<T, U> = T & { __brand: U };
type Flavor<T, U> = T & { __flavor?: U };

type Nominal<T, U> = T extends object ? Flavor<T, U> : Brand<T, U>;

Case studies

Do you want support to launch your digital project?

Submit your project now