Reading — step 1 of 5
Learn
~1 min readBranded Types and Guards
TypeScript's structural typing means string is string everywhere. Branded types let you create distinct types that share a runtime representation.
The problem:
function sendEmail(userId: string, email: string) { ... }
sendEmail('[email protected]', 'user-123'); // SWAPPED — TypeScript can't catch this
The fix with brands:
type UserId = string & { readonly __brand: unique symbol };
type Email = string & { readonly __brand: unique symbol };
function asUserId(s: string): UserId {
// (validate format, throw on bad input)
return s as UserId;
}
function asEmail(s: string): Email {
if (!s.includes('@')) throw new Error('not an email');
return s as Email;
}
function sendEmail(userId: UserId, email: Email) { ... }
sendEmail(asUserId('user-123'), asEmail('[email protected]')); // OK
sendEmail('[email protected]', 'user-123'); // ERROR — not branded
The unique symbol brand has zero runtime existence — pure type-level distinction.
Common use cases:
- Database IDs (UserId vs PostId vs OrderId)
- Validated strings (Email, URL, NonEmptyString, ISODateString)
- Numeric units (Pixels vs Inches vs Seconds)
- Currencies (USD vs EUR — never accidentally mix)
type Brand<T, B> = T & { readonly __brand: B };
type USD = Brand<number, 'USD'>;
type EUR = Brand<number, 'EUR'>;
function toUSD(n: number): USD { return n as USD; }
function toEUR(n: number): EUR { return n as EUR; }
function addUSD(a: USD, b: USD): USD { return (a + b) as USD; }
const price = toUSD(10);
const tax = toUSD(2);
addUSD(price, tax); // OK
// addUSD(price, toEUR(2)); // ERROR
TypeScript 4.7+ has unique symbol for stricter brands. TypeScript 5.0+ added accessor syntax for cleaner brand patterns.
This pattern is used heavily in big TypeScript codebases (Stripe, Sentry, Microsoft) to prevent ID-confusion bugs.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…