| name | branded-types |
| description | Use when primitive types need semantic distinction. Use when string or number types have different meanings. Use when you need nominal typing. |
Consider Brands for Nominal Typing
Overview
Add phantom types to distinguish semantically different values.
TypeScript uses structural typing, but sometimes you need nominal typing - values that are distinct because you SAY they are, not because they have different shapes. Brands let you do this without runtime overhead.
When to Use This Skill
- Distinguishing paths (absolute vs relative)
- Attaching units to numbers (meters, seconds)
- Preventing 2D/3D vector mix-ups
- Marking validated/sanitized strings
- Creating type-safe identifiers
The Iron Rule
ALWAYS use brands when primitive types have different semantic meanings.
Remember:
- Brands are phantom types (exist only in type system)
- No runtime overhead
- Force explicit conversion/validation
- Make invalid states unrepresentable
Detection: The "Wrong Primitive" Problem
When different primitives can be confused:
function readFile(path: string) { ... }
readFile('foo.txt');
readFile('/home/foo.txt');
The Branding Pattern
Basic Brand Structure
type AbsolutePath = string & { _brand: 'abs' };
function isAbsolutePath(path: string): path is AbsolutePath {
return path.startsWith('/');
}
function listAbsolutePath(path: AbsolutePath) {
}
Using Branded Types
function f(path: string) {
if (isAbsolutePath(path)) {
listAbsolutePath(path);
}
listAbsolutePath(path);
}
Why Brands Work
You can't actually create a value that is both a string and has a _brand property:
type AbsolutePath = string & { _brand: 'abs' };
The only way to get an AbsolutePath is to:
- Be given one (from a function that returns it)
- Use a type guard to validate and narrow
- Use a type assertion (escape hatch)
Common Brand Patterns
Units of Measurement
type Meters = number & { _brand: 'meters' };
type Seconds = number & { _brand: 'seconds' };
type MetersPerSecond = number & { _brand: 'm/s' };
const meters = (m: number) => m as Meters;
const seconds = (s: number) => s as Seconds;
function calculateSpeed(distance: Meters, time: Seconds): MetersPerSecond {
return (distance / time) as MetersPerSecond;
}
const d = meters(100);
const t = seconds(10);
const speed = calculateSpeed(d, t);
calculateSpeed(100, 10);
calculateSpeed(t, d);
Caveat: Arithmetic operations lose the brand:
const doubled = d * 2;
Validated Strings
type SanitizedHTML = string & { _brand: 'sanitized' };
type UserId = string & { _brand: 'userId' };
type Email = string & { _brand: 'email' };
function sanitize(html: string): SanitizedHTML {
return html.replace(/<script>/g, '') as SanitizedHTML;
}
function setInnerHTML(el: Element, html: SanitizedHTML) {
el.innerHTML = html;
}
setInnerHTML(el, '<script>alert("xss")</script>');
setInnerHTML(el, sanitize(userInput));
Type-Safe IDs
type UserId = string & { _brand: 'userId' };
type PostId = string & { _brand: 'postId' };
function getUser(id: UserId): User { ... }
function getPost(id: PostId): Post { ... }
declare const userId: UserId;
declare const postId: PostId;
getUser(userId);
getUser(postId);
Sorted Arrays
type SortedList<T> = T[] & { _brand: 'sorted' };
function isSorted<T>(xs: T[]): xs is SortedList<T> {
for (let i = 0; i < xs.length - 1; i++) {
if (xs[i] > xs[i + 1]) return false;
}
return true;
}
function binarySearch<T>(xs: SortedList<T>, x: T): boolean {
let low = 0, high = xs.length - 1;
while (high >= low) {
const mid = low + Math.floor((high - low) / 2);
const v = xs[mid];
if (v === x) return true;
[low, high] = x > v ? [mid + 1, high] : [low, mid - 1];
}
return false;
}
const nums = [1, 3, 5, 7, 9];
if (isSorted(nums)) {
binarySearch(nums, 5);
}
Alternative Branding Techniques
Using Unique Symbol (Stronger)
declare const brand: unique symbol;
type Meters = number & { [brand]: 'meters' };
Using Private Fields in Classes
class ValidatedEmail {
private readonly _brand!: 'email';
constructor(public readonly value: string) {
if (!value.includes('@')) throw new Error('Invalid email');
}
}
Preventing Vector Mix-ups
interface Vector2D {
x: number;
y: number;
z?: never;
}
function norm(v: Vector2D) {
return Math.sqrt(v.x ** 2 + v.y ** 2);
}
const v3d = { x: 3, y: 4, z: 5 };
norm(v3d);
Or use brands:
type Vector2D = { x: number; y: number } & { _brand: '2d' };
type Vector3D = { x: number; y: number; z: number } & { _brand: '3d' };
Pressure Resistance Protocol
1. "Just Use Type Aliases"
Pressure: "Type alias is simpler: type UserId = string"
Response: Type aliases don't prevent mixing up different string types.
Action: Use brands when semantic distinction matters.
2. "It's Just Runtime Overhead"
Pressure: "Adding properties to primitives costs memory"
Response: Brands are phantom types - they don't exist at runtime.
Action: Use brands freely; there's no runtime cost.
Red Flags - STOP and Reconsider
- Multiple string/number types that could be confused
- Functions that accept "any string" but expect specific formats
- Validation that happens but isn't tracked in the type system
- Bugs from swapping similarly-typed arguments
Common Rationalizations (All Invalid)
| Excuse | Reality |
|---|
| "We'll be careful" | Mistakes happen, especially in large codebases |
| "Type alias is enough" | Aliases don't prevent cross-assignment |
| "Too much ceremony" | Prevents bugs that are hard to track down |
Quick Reference
type Brand<T, B extends string> = T & { _brand: B };
type UserId = Brand<string, 'userId'>;
type Meters = Brand<number, 'meters'>;
function isX(val: T): val is BrandedT { ... }
const meters = (n: number) => n as Meters;
The Bottom Line
Use brands to give semantic meaning to primitives.
Brands add no runtime overhead but prevent mixing up values that happen to have the same underlying type. Use them for IDs, paths, units, validated strings, and any primitive where semantic distinction matters.
Reference
Based on "Effective TypeScript" by Dan Vanderkam, Item 64: Consider Brands for Nominal Typing.