TypeScript
Completed
-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
61: Branded and Nominal Types in TypeScript
I want to show you a bug that has caused some genuinely terrifying production outages in my past. It’s the kind of bug that passes every single TypeScript check, passes your CI/CD pipeline, and only blows up when a user accidentally deletes the wrong record from your database.
type UserId = string;
type OrderId = string;
function cancelOrder(orderId: OrderId) {
console.log(`Cancelling order ${orderId}...`);
// API call to cancel order
}
function archiveUser(userId: UserId) {
console.log(`Archiving user ${userId}...`);
// API call to archive user
}
const currentUserId: UserId = "user_abc123";
const currentOrderId: OrderId = "order_xyz789";
// Oops! I swapped the arguments.
cancelOrder(currentUserId);
archiveUser(currentOrderId);
The structural typing trap
If you look at the code above, you'll notice something frustrating: TypeScript doesn't complain. Not a single red squiggly line. Why? Because TypeScript uses structural typing.
To TypeScript, UserId and OrderId aren't actually different types; they are just aliases for string. Since a string is a string, TypeScript sees no problem with passing a User ID into a function that expects an Order ID. In a small project, this is fine. In a codebase with 50 different types of IDs (Product IDs, Session IDs, Transaction IDs), it's a landmine.
If you're coming from Java or C#, you're used to nominal typing, where two classes with the exact same fields are still considered different types because they have different names. TypeScript doesn't do that by default, but we can fake it using a technique called "Branding."
Creating "Branded" Types
The trick is to tell TypeScript that a string isn't just a string—it's a string that also has a special, invisible property that only exists for the compiler. We call this a "brand."
type Brand<K, T> = T & { __brand: K };
type UserId = Brand<'UserId', string>;
type OrderId = Brand<'OrderId', string>;
function cancelOrder(orderId: OrderId) {
console.log(`Cancelling order ${orderId}...`);
}
const currentUserId = "user_abc123" as UserId;
const currentOrderId = "order_xyz789" as OrderId;
// Now this throws a compiler error!
// Type 'UserId' is not assignable to type 'OrderId'.
cancelOrder(currentUserId);
Here is what's happening: we've created an intersection type. UserId is now "a string AND an object with a property called __brand that equals 'UserId'". Since OrderId has a brand of 'OrderId', they are no longer compatible. I personally love this pattern because it pushes the "danger" to the edges of my application.
Handling the "Type Casting" trade-off
You probably noticed I used as UserId. This is the one "ugly" part of branded types. Since the __brand property doesn't actually exist at runtime (it's a phantom), you can't just assign a raw string to a branded type. You have to cast it.
I usually handle this by creating "Type Guards" or "Constructor" functions. Instead of scattering as casts throughout my business logic, I wrap the creation of these types in a single place:
function createUserId(id: string): UserId {
return id as UserId;
}
function createOrderId(id: string): OrderId {
return id as OrderId;
}
// Now the rest of my app is type-safe
const user = createUserId("user_123");
const order = createOrderId("order_456");
By doing this, you've essentially created a "nominal" system. You've told TypeScript: "I don't care if these are both strings underneath; treat them as completely different entities." It adds a tiny bit of boilerplate, but it saves you from the nightmare of accidentally deleting a user when you meant to cancel an order.
📋 Practical Task
Exercise: Implementing a Type-Safe Currency System
You are building a financial application that handles multiple currencies (USD, EUR, and GBP). Currently, the app uses number for everything, which has led to bugs where developers are adding USD amounts to EUR amounts without converting them first.
Your task:
- Create a
Brandutility type. - Define three branded types:
USD,EUR, andGBP(all based onnumber). - Create a function
calculateTotal(amount1: USD, amount2: USD): USDthat adds two USD amounts together. - Create a "constructor" function for each currency (e.g.,
toUSD(val: number): USD). - Verify that attempting to pass a
EURvalue intocalculateTotalcauses a TypeScript compilation error.
There are no comments for now.