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
- Use for domain types - When you need distinct types
- Validate on creation - Ensure values are valid
- Provide constructors - Don't expose type assertions
- Document the brand - Explain why types are distinct
- Use sparingly - Only when needed for safety
Common Mistakes
- 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;
}
- 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 { }
- 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.