Create distinct types from primitive types to prevent mixing.

Branded Types

Add a "brand" to distinguish types.

type Brand<T, B> = T & { __brand: B };

type UserId = Brand<string, "UserId">;
type ProductId = Brand<string, "ProductId">;

function getUser(id: UserId): User {
  // ...
}

function getProduct(id: ProductId): Product {
  // ...
}

const userId: UserId = "123" as UserId;
const productId: ProductId = "456" as ProductId;

getUser(userId);     // OK
getUser(productId);  // Error: Type 'ProductId' is not assignable to type 'UserId'

Opaque Types

Hide the underlying type completely.

declare const __opaque: unique symbol;
type Opaque<T, K> = T & { readonly [__opaque]: K };

type Email = Opaque<string, "Email">;
type Password = Opaque<string, "Password">;

function createEmail(value: string): Email {
  if (!value.includes("@")) {
    throw new Error("Invalid email");
  }
  return value as Email;
}

function createPassword(value: string): Password {
  if (value.length < 8) {
    throw new Error("Password too short");
  }
  return value as Password;
}

const email: Email = createEmail("user@example.com");
const password: Password = createPassword("secure123");

// email and password are distinct types

Nominal Typing

TypeScript uses structural typing, but branded types provide nominal typing.

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

function convertToFeet(meters: Meters): Feet {
  return (meters * 3.28084) as Feet;
}

const distance: Meters = 10 as Meters;
const inFeet = convertToFeet(distance); // Type-safe conversion

Type-Safe IDs

type EntityId<T> = Brand<string, T>;

type UserId = EntityId<"User">;
type PostId = EntityId<"Post">;
type CommentId = EntityId<"Comment">;

interface User {
  id: UserId;
  name: string;
}

interface Post {
  id: PostId;
  title: string;
  authorId: UserId;
}

function getUser(id: UserId): User | undefined {
  // Implementation
  return undefined;
}

const userId: UserId = "user-123" as UserId;
const postId: PostId = "post-456" as PostId;

getUser(userId);  // OK
getUser(postId);  // Error: Type 'PostId' is not assignable to type 'UserId'

Exercises

Exercise 1: Branded Types

// Create branded types for:
// - Temperature (Celsius and Fahrenheit)
// - Create conversion functions
// - Ensure type safety

Exercise 2: Opaque Types

// Create opaque types for:
// - Email addresses
// - URLs
// - Create validation functions

Exercise 3: Entity IDs

// Create a generic EntityId type
// Use it for different entity types
// Ensure IDs can't be mixed

Exercise 4: Money Type

// Create a branded Money type
// Support different currencies
// Prevent mixing currencies

Exercise 5: Validation

// Create opaque types with validation
// Ensure invalid values can't be created
// Provide safe constructors

Best Practices

  1. Use for domain types - When you need distinct types
  2. Validate on creation - Ensure values are valid
  3. Provide constructors - Don't expose type assertions
  4. Document the brand - Explain why types are distinct
  5. Use sparingly - Only when needed for safety

Common Mistakes

  1. Exposing type assertions
// ❌ Bad - allows invalid values
type Email = Brand<string, "Email">;
const email: Email = "not-an-email" as Email;

// ✅ Good - validate on creation
function createEmail(value: string): Email {
  if (!isValidEmail(value)) {
    throw new Error("Invalid email");
  }
  return value as Email;
}
  1. Not using branded types when needed
// ❌ IDs can be mixed
function getUser(id: string): User { }
function getPost(id: string): Post { }

// ✅ Type-safe IDs
function getUser(id: UserId): User { }
function getPost(id: PostId): Post { }
  1. Forgetting the brand in operations
// ❌ Loses brand
function add(a: Meters, b: Meters): number {
  return a + b; // Returns number, not Meters
}

// ✅ Preserve brand
function add(a: Meters, b: Meters): Meters {
  return (a + b) as Meters;
}

Next Steps

Learn about Type System Hacks and Workarounds.