Lab 13: Branded Types & Security

Time: 40 minutes | Level: Advanced | Docker: docker run -it --rm node:20-alpine sh

Use branded (nominal) types to prevent security vulnerabilities at compile time: SQL injection, unsafe URLs, secret leakage, ID mixing, and more.


Step 1: Environment Setup

docker run -it --rm node:20-alpine sh
npm install -g typescript ts-node
mkdir lab13 && cd lab13
npm init -y
echo '{"compilerOptions":{"module":"commonjs","target":"es2020","strict":true}}' > tsconfig.json

💡 TypeScript uses structural typing by default: any two types with the same structure are interchangeable. Branded types add nominal typing — a UserId and a ProductId are both string, but TypeScript treats them as incompatible.


Step 2: Basic Branded Types

The core brand pattern:

// brands.ts

// Method 1: Intersection with a phantom type
type Brand<T, B extends string> = T & { readonly __brand: B };

// Convenience type aliases
type UserId   = Brand<string, 'UserId'>;
type ProductId = Brand<string, 'ProductId'>;
type OrderId  = Brand<string, 'OrderId'>;
type Email    = Brand<string, 'Email'>;
type PositiveNumber = Brand<number, 'PositiveNumber'>;

// Factory functions (the only way to create branded values)
function asUserId(id: string): UserId       { return id as UserId; }
function asProductId(id: string): ProductId { return id as ProductId; }
function asEmail(s: string): Email {
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s)) throw new Error('Invalid email');
  return s as Email;
}
function asPositiveNumber(n: number): PositiveNumber {
  if (n <= 0) throw new Error('Must be positive');
  return n as PositiveNumber;
}

// Functions that require specific brands
function getUserById(id: UserId): string { return `User: ${id}`; }
function getProductById(id: ProductId): string { return `Product: ${id}`; }
function sendEmail(to: Email, subject: string): void {
  console.log(`Sending email to ${to}: ${subject}`);
}

const uid = asUserId('usr_abc123');
const pid = asProductId('prod_xyz789');
const email = asEmail('[email protected]');

// ✅ Correct usage
console.log(getUserById(uid));
console.log(getProductById(pid));
sendEmail(email, 'Welcome!');

// ❌ These would be compile-time errors (uncomment to see):
// getUserById(pid);         // Error: ProductId is not assignable to UserId
// getProductById(uid);      // Error: UserId is not assignable to ProductId
// getUserById('raw-string'); // Error: string is not assignable to UserId

console.log('Branded types verified!');

Step 3: SQL Injection Prevention

Brand SQL strings to prevent passing raw user input to the database:

💡 In production, combine this with a query builder (like Drizzle ORM or Kysely) that returns branded types automatically. You never write raw SQL strings.


Step 4: Secret Type — Prevent JSON Serialization

Prevent sensitive values from leaking into logs or API responses:


Step 5: URL Brand for XSS Prevention

Brand sanitized URLs to prevent XSS via javascript: URLs:


Step 6: Combining Multiple Brands

Real-world scenario: typed IDs prevent accidental mixing in a multi-entity system:


Step 7: Validated Types Pattern

Combine runtime validation with compile-time brands:


Step 8: Capstone — Security-First API Layer

Run:

📸 Verified Output:


Summary

Brand Pattern
Code
Security Benefit

ID brands

type UserId = string & {_brand:'UserId'}

Prevent ID parameter mixing

SQL branding

type SqlQuery = string & {_brand:'SqlQuery'}

Prevent raw SQL injection

Secret wrapper

class Secret<T> with toJSON():never

Prevent credential leakage

URL sanitization

type SafeUrl = string & {_brand:'SafeUrl'}

Prevent XSS via javascript:

Validated type

Multi-brand intersection

Enforce validation chain

Factory functions

const asUserId = (s) => s as UserId

Single point of trust

Last updated