Skip to content

Branded Types Usage Guide

Overview

Branded types (nominal types) are a TypeScript pattern that creates type-safe distinctions between values that have the same runtime representation but different semantic meanings.

Why Use Branded Types?

typescript
// WITHOUT branded types - these are all just strings
const projectId = "proj_123";
const noteId = "note_456";
const sceneId = "scene_789";

// Easy to mix them up!
function getNote(id: string) { ... }
getNote(projectId); // ❌ Wrong ID type, but TypeScript allows it!

// WITH branded types
const projectId: ProjectId = "proj_123" as ProjectId;
const noteId: NoteId = "note_456" as NoteId;

function getNote(id: NoteId) { ... }
getNote(projectId); // ✅ TypeScript error! Can't use ProjectId where NoteId is expected

Available Branded Types

ID Types

  • ProjectId - Cloud project identifiers
  • NoteId - Note document identifiers
  • SceneId - Scene identifiers
  • ActId - Act identifiers
  • CharacterId - Character identifiers
  • CastMemberId - Cast/crew member identifiers

Timestamp Types

  • ISOTimestamp - ISO 8601 timestamp strings
  • FirestoreTimestamp - Firestore timestamp objects

Metadata Types

  • Metadata - Generic metadata objects
  • AppMetadata - Application-specific metadata

How to Use

1. Type Assertion (Quick & Simple)

typescript
import { ProjectId, NoteId } from '@/types/branded';

const projectId = cloudProjectId as ProjectId;
const noteId = crypto.randomUUID() as NoteId;
typescript
import { toProjectId, toISOTimestamp, toMetadata } from '@/types/branded';

// Safe conversion with validation
const projectId = toProjectId('proj_abc123');

// Create ISO timestamp from current time
const timestamp = toISOTimestamp();

// Create ISO timestamp from Date
const customTime = toISOTimestamp(new Date('2026-01-22'));

// Create metadata with type safety
const metadata = toMetadata({
  customField: 'value',
  timestamp: Date.now()
});

3. Type Guards (Runtime Checking)

typescript
import { isProjectId, isISOTimestamp, isMetadata } from '@/types/branded';

if (isProjectId(value)) {
  // TypeScript knows value is ProjectId here
  useProject(value);
}

if (isISOTimestamp(dateString)) {
  // Safe to use as ISO timestamp
  const date = new Date(dateString);
}

Real-World Examples

Example 1: Function Parameters

typescript
// Before - easy to mix up IDs
function deleteNote(projectId: string, noteId: string) {
  // Which is which? Easy to swap!
}

// After - compile-time safety
function deleteNote(projectId: ProjectId, noteId: NoteId) {
  // Can't accidentally swap these!
}

deleteNote(noteId, projectId); // ✅ TypeScript error!

Example 2: API Responses

typescript
interface CloudProject {
  id: ProjectId;  // Not just any string!
  ownerId: CastMemberId;
  createdAt: ISOTimestamp;
  metadata: AppMetadata;
}

// Type safety when using the response
const project: CloudProject = await fetchProject();
updateProjectMeta(project.id, project.metadata); // ✅ All types match!

Example 3: State Management

typescript
interface NotesState {
  activeNoteId: NoteId | null;
  notes: Map<NoteId, Note>;
}

// Operations are type-safe
function setActive Note(noteId: NoteId) {
  state.activeNoteId = noteId; // ✅
}

setActiveNote(sceneId); // ❌ TypeScript error!

Migration Strategy

Phase 1: Add Types (Non-breaking)

typescript
// Keep existing code working, add types gradually
export function getProject(id: string | ProjectId) {
  const projectId = typeof id === 'string' ? toProjectId(id) : id;
  // ...
}

Phase 2: Enforce at Boundaries

typescript
// API boundaries should use branded types
export async function saveCloudProject(
  projectId: ProjectId,  // Enforce at API level
  data: RunSheetState
) {
  // ...
}

Phase 3: Full Adoption

typescript
// Eventually, all ID handling uses branded types
const notes = new Map<NoteId, Note>();
const scenes = new Map<SceneId, Scene>();

// No more ID confusion!

Benefits

  1. Compile-Time Safety: Catch ID mix-ups before runtime
  2. Self-Documenting: Type names explain the semantic meaning
  3. Refactoring Support: TypeScript helps you find all usages
  4. Zero Runtime Cost: Brands are erased at compile time
  5. IDE Support: Better autocomplete and error messages

Best Practices

  1. ✅ Use helper functions (toProjectId) for validation
  2. ✅ Apply brands at system boundaries (API calls, database)
  3. ✅ Use type guards when dealing with external data
  4. ❌ Don't overuse - only for semantically distinct concepts
  5. ❌ Don't brand every string - only when safety matters

When NOT to Use

  • Simple utility strings (display text, labels)
  • Temporary variables in local scope
  • Data that genuinely can be any string
  • Performance-critical hot paths (though cost is minimal)

Further Reading