Skip to content

TypeScript Branded Types — Complete Guide

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about TypeScript Branded Types. We cover key concepts, practical examples, and best practices to help you master this topic.

TypeScript branded types simulate nominal typing in a structural type system — they add a phantom property that distinguishes structurally identical types at compile time without any runtime overhead, preventing accidental mixing of IDs, currencies, and units.

What You'll Learn

  • Branded type pattern with intersection types
  • The unique symbol approach
  • Currency and unit safety
  • ID type safety with branded strings
  • When to brand vs use plain types

Why It Matters

In a large codebase, mixing up a userId with a scanId is a real source of bugs. Both are strings, but they represent different concepts. Branded types make these swaps a compile-time error without any runtime cost.

Real-World Use

Durga Antivirus Pro uses UserId, ScanId, and ThreatId branded types throughout its codebase. A function expecting a ScanId cannot accidentally receive a UserId, even though both are strings. This has prevented dozens of bugs in production.

Learning Path

flowchart LR
  A[Overloads] --> B[Branded Types]
  B --> C[tsconfig Deep Dive]
  B --> D[You Are Here]
  C --> E[Project References]
  E --> F[Module Resolution]

Basic Brand Pattern

declare const brand: unique symbol;
type Brand<T, B extends string> = T & { [brand]: B };

type UserId = Brand<string, "UserId">;
type ScanId = Brand<string, "ScanId">;
type Email = Brand<string, "Email">;

function createUserId(id: string): UserId { return id as UserId; }
function createScanId(id: string): ScanId { return id as ScanId; }

function getUser(id: UserId): void { console.log(`User: ${id}`); }
function getScan(id: ScanId): void { console.log(`Scan: ${id}`); }

const uid = createUserId("usr-001");
const sid = createScanId("scan-001");

getUser(uid);   // OK
// getUser(sid); // Error: ScanId is not UserId

Think of brands as color-coded labels — two white T-shirts look identical, but one has a blue tag (UserId) and another a red tag (ScanId). The tags are invisible to anyone not looking for them (at runtime), but the sorting machine (the compiler) can distinguish them perfectly.

Currency Safety

type USD = Brand<number, "USD">;
type EUR = Brand<number, "EUR">;
type GBP = Brand<number, "GBP">;

function usd(amount: number): USD { return amount as USD; }
function eur(amount: number): EUR { return amount as EUR; }
function gbp(amount: number): GBP { return amount as GBP; }

function addUSD(a: USD, b: USD): USD { return (a + b) as USD; }
function convertToUSD(eur: EUR, rate: number): USD { return (eur * rate) as USD; }

const price = usd(29.99);
const tax = usd(3.00);
const total = addUSD(price, tax);  // OK

const euroCost = eur(25.00);
// addUSD(price, euroCost);         // Error: EUR is not USD

const converted = convertToUSD(euroCost, 1.08);
console.log(`Total: $${total + converted}`); // type: number (brand lost after arithmetic)

Unit Safety

type Meters = Brand<number, "Meters">;
type Feet = Brand<number, "Feet">;
type Seconds = Brand<number, "Seconds">;

function meters(v: number): Meters { return v as Meters; }
function feet(v: number): Feet { return v as Feet; }

function toFeet(m: Meters): Feet { return (m * 3.28084) as Feet; }

const roomWidth = meters(10);
const roomHeight = feet(32.8);

// toFeet(roomHeight); // Error: Feet is not Meters
console.log(`Width in feet: ${toFeet(roomWidth)}`);

ID Type Safety

type ArticleId = Brand<string, "ArticleId">;
type CommentId = Brand<string, "CommentId">;

declare function getArticle(id: ArticleId): Promise<Article>;
declare function getComment(id: CommentId): Promise<Comment>;

const articleId = "art-123" as ArticleId;
const commentId = "cmt-456" as CommentId;

getArticle(articleId);   // OK
// getArticle(commentId); // Error

// Cannot accidentally swap in API calls
function deleteResource(id: ArticleId | CommentId): void {
  // ...
}

Advanced: Nominal Classes

class Nominal<Tag extends string> {
  private readonly __tag!: Tag;
}

type Meters = Nominal<"Meters"> & { value: number };
type Kilograms = Nominal<"Kilograms"> & { value: number };

function calculateForce(mass: Kilograms, acceleration: Meters): number {
  return mass.value * acceleration.value;
}

const m = { value: 5 } as Meters;
const kg = { value: 10 } as Kilograms;
calculateForce(kg, m); // OK
// calculateForce(m, kg); // Error

Common Mistakes

1. Brands Lost After Operations

type USD = Brand<number, "USD">;
const a = 100 as USD;
const b = a * 2;        // type: number — brand lost!
const c = (a * 2) as USD; // Must re-brand

2. Runtime Brand Access

Brands are compile-time only. brand symbol does not exist at runtime.

3. Forgetting as Casts in Factory Functions

Always provide factory functions to create branded values safely.

4. Over-Branding Simple Values

Not every value needs a brand. Use brands only when mixing up types would cause bugs.

5. Brand Collisions

Use unique symbol to prevent brand collisions across modules.

Practice Questions

  1. What is the purpose of branded types? To distinguish structurally identical types at compile time, preventing accidental mixing.

  2. What is unique symbol used for? To ensure the brand property is unique across the entire codebase, preventing collision.

  3. Do brands exist at runtime? No. Brands are erased during compilation and have zero runtime cost.

  4. When should you NOT use branded types? When the type distinction is already enforced by the type system (e.g., different interfaces with different properties).

Challenge: Create branded types for a cooking recipe system with Teaspoon, Tablespoon, and Cup. Write functions to add same-unit measurements and convert between units with compile-time safety.

FAQ

Do branded types affect bundle size?

No. They are compile-time only and erased in the output JavaScript.

What is the `unique symbol` type?

A TypeScript feature that guarantees a symbol is unique across all files and modules.

Can I brand object types?

Yes: type AdminUser = Brand<User, "Admin">;

How do I create a branded array?

type TaggedArray<T> = Brand<T[], "Tag">;

Can brands be composed?

Yes: type VerifiedEmail = Brand<Email, "Verified">;

Mini Project: Type-Safe Shopping Cart

declare const brand: unique symbol;
type Brand<T, B extends string> = T & { [brand]: B };

type USD = Brand<number, "USD">;
type ItemId = Brand<string, "ItemId">;
type CartId = Brand<string, "CartId">;

function usd(v: number): USD { return v as USD; }
function itemId(v: string): ItemId { return v as ItemId; }

interface CartItem {
  id: ItemId;
  name: string;
  price: USD;
  quantity: number;
}

class ShoppingCart {
  private items: CartItem[] = [];
  constructor(public readonly id: CartId) {}

  addItem(item: CartItem): void { this.items.push(item); }
  getTotal(): USD {
    const total = this.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
    return total as USD;
  }
  getItemCount(): number {
    return this.items.reduce((sum, item) => sum + item.quantity, 0);
  }
}

const cart = new ShoppingCart("cart-abc" as CartId);
cart.addItem({ id: itemId("item-1"), name: "Scanner License", price: usd(49.99), quantity: 1 });
cart.addItem({ id: itemId("item-2"), name: "VPN Subscription", price: usd(29.99), quantity: 2 });

console.log(`Cart total: $${cart.getTotal()}`);  // $109.97
console.log(`Items: ${cart.getItemCount()}`);     // 3

What's Next

You've completed Module 4: Type System Deep Dive. Now explore Tooling & Config:

Lesson Description
{{< ref "/programming-languages/typescript/29-overloads-hybrid" >}} Review overloads
{{< ref "/programming-languages/typescript/31-tsconfig-deep-dive" >}} strict, noImplicitAny, target, module
{{< ref "/programming-languages/typescript/32-project-references" >}} Composite projects and build mode

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro