Skip to content

Branding & Flavoring

branding.ts

Definition : Trick that makes a type structurally distinct and strictly unassignable without explicit conversion, by adding a mandatory property (__brand).

Usage :

  • Primitive Type
  • Explicit Conversion

Ex: Critical Values (BearerToken Vs. AccessToken)

ts
export type Brand<T, U> = T & { __brand: U };

type PostId = string & { __brand: "PostId" };
type CommentId = Brand<string, "CommentId">;

// Strict Branding Factory
const makePostId = (id: string): PostId => id as PostId;
// TODO: const makeCommentId = (id: string): CommentId => id as CommentId;

const value = makePostId(42);

const postId: PostId = value; // Ok
const commentId: CommentId = value; // Error

flavoring.ts

Definition : Lighter variant of "Branding" that makes a type semantically distinct while remaining structurally compatible, thanks to an optional property (__flavor).

Usage :

  • Object Type
  • Implicit Conversion

Ex: Semantic Differences (User Vs. UserRoles)

ts
export type Flavor<T, U> = T & { __flavor?: U };

type Post = { author: string; content: string } & { __flavor?: "Post" };
type PostComment = Flavor<{ author: string; content: string }, "PostComment">;

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

const post: Post = { author: "John Doe", content: "Lorem ipsum dolor sit amet" };
createPost(post); // Ok

const postComment: PostComment = { author: "John Doe", content: "Lorem ipsum dolor sit amet" };
createPost(postComment); // Error

nominal.ts

ts
import { type Brand } from "./branding";
import { type Flavor } from "./flavoring";

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

type AccessToken = Nominal<string, "AccessToken">; // Brand
type User = Nominal<{ login: string; pswd: string; roles: string[] }, "User">; // Flavor
type UserRoles = Nominal<{ roles: string[] }, "UserRoles">; // Flavor

Released under the MIT License.