-
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
119: Enforcing Type Boundaries Between Modules
Think of a professional kitchen. When you're a customer, you interact with a menu. The menu is the "public API." You order a "Medium-Rare Ribeye," and the kitchen delivers it. You don't know—and shouldn't care—which brand of grill they used, how the walk-in freezer is organized, or the specific internal shorthand the chefs use to communicate. If the chef decides to switch from a gas grill to charcoal, your experience of ordering a ribeye remains exactly the same. The boundary between the dining room and the kitchen is strictly enforced.
In TypeScript, we often accidentally let the customers walk right into the kitchen. We export internal interfaces or use generic types (like string or number) for everything. This creates a "leaky boundary." If you use a string for a UserId, a ProductId, and an OrderNumber, TypeScript will let you pass a ProductId into a function that expects a UserId. The compiler is happy, but your database is about to scream.
The Danger of the Generic String
I've seen this happen in huge codebases: a developer creates a PaymentService and a UserService. Both use string for their identifiers. Eventually, someone writes a cleanup script and accidentally passes a userId into paymentService.refundTransaction(transactionId). Because both are just strings, the code compiles perfectly, and you end up trying to refund a user instead of a transaction.
// The "Leaky" Way
export type UserId = string;
export type TransactionId = string;
export function refundTransaction(tid: TransactionId) {
/* ... */
}
const myUserId: UserId = "user_123";
// TypeScript allows this! It's just a string.
refundTransaction(myUserId);
Branding Your Types for Strictness
To stop this, we need to "brand" our types. Since TypeScript uses structural typing (if it looks like a duck, it's a duck), we have to add a unique "mark" to the type that doesn't actually exist at runtime but tells the compiler: "This isn't just any string; it's specifically a TransactionId."
Here is how I typically handle this. We use an intersection type with a unique property that can never actually be assigned a value.
// The "Enforced Boundary" Way
export type Brand<K, T> = T & { __brand: K };
export type UserId = Brand<"UserId", string>;
export type TransactionId = Brand<"TransactionId", string>;
// We need a "Type Guard" or a casting function to create these
export function createTransactionId(id: string): TransactionId {
return id as TransactionId;
}
export function refundTransaction(tid: TransactionId) {
console.log(`Refunding ${tid}`);
}
const myUserId = "user_123" as UserId;
const myTxId = createTransactionId("tx_999");
refundTransaction(myTxId); // Works great.
// refundTransaction(myUserId); // ERROR: Type 'UserId' is not assignable to 'TransactionId'.
Protecting the Implementation Leak
Beyond just IDs, you should be careful about exporting your internal data models. I always recommend creating a "Public" version of a type and an "Internal" version. The internal version might have sensitive fields or complex logic, while the public version is a stripped-down interface.
If you export your internal database entity directly, every single component that uses that entity is now coupled to your database schema. If you rename a column in your DB, you might have to update 50 different files in your frontend. Instead, map your internal type to a public-facing one at the boundary of your module. It's a bit more boilerplate upfront, but it saves you from a refactoring nightmare six months down the line.
📋 Practical Task
Exercise: Implementing a Secure Session Token Guard
You are building an authentication module. Currently, the system uses plain strings for SessionTokens and ApiKeys, which is causing bugs where API keys are being passed into session-validation functions.
Your Task:
- Implement a
Brandtype utility that can be used to differentiate between different string-based identifiers. - Create two branded types:
SessionTokenandApiKey. - Create a factory function
createSessionToken(token: string): SessionTokenthat casts a raw string into a brandedSessionToken. - Create a function
validateSession(token: SessionToken): booleanthat only accepts the brandedSessionToken. - Demonstrate that passing a raw string or an
ApiKeyintovalidateSessionresults in a TypeScript compilation error.
There are no comments for now.