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 expectedAvailable Branded Types
ID Types
ProjectId- Cloud project identifiersNoteId- Note document identifiersSceneId- Scene identifiersActId- Act identifiersCharacterId- Character identifiersCastMemberId- Cast/crew member identifiers
Timestamp Types
ISOTimestamp- ISO 8601 timestamp stringsFirestoreTimestamp- Firestore timestamp objects
Metadata Types
Metadata- Generic metadata objectsAppMetadata- 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;2. Helper Functions (Recommended - With Validation)
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
- Compile-Time Safety: Catch ID mix-ups before runtime
- Self-Documenting: Type names explain the semantic meaning
- Refactoring Support: TypeScript helps you find all usages
- Zero Runtime Cost: Brands are erased at compile time
- IDE Support: Better autocomplete and error messages
Best Practices
- ✅ Use helper functions (
toProjectId) for validation - ✅ Apply brands at system boundaries (API calls, database)
- ✅ Use type guards when dealing with external data
- ❌ Don't overuse - only for semantically distinct concepts
- ❌ 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)