-
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
117: Structuring a Large TypeScript Codebase
When developers first move from small projects to enterprise-scale TypeScript codebases, they usually hit a wall with organization. I see it all the time: a developer decides to "be clean" by creating a single folder called /types or a massive file named types.ts where every single interface and type alias in the entire application lives.
The "Centralized Types" Trap
It feels intuitive. You think, "I'll put all my shapes in one place so I can find them easily." But in a large codebase, this is a recipe for disaster. Let's look at a hypothetical E-commerce system. You have User, Order, Product, and Payment types. If they all live in one types.ts file, that file becomes a "god object" that every single other file in your system must import.
// The problematic types.ts
export interface User { id: string; name: string; }
export interface Order { id: string; userId: string; items: OrderItem[]; }
export interface OrderItem { productId: string; quantity: number; }
export interface Product { id: string; price: number; }
export interface Payment { id: string; orderId: string; status: 'paid' | 'pending'; }
// ... 500 more lines of types
The moment you do this, you've created a massive bottleneck. If you change one property in Payment, TypeScript may have to re-evaluate every single file that imports types.ts, even the files that only care about User. Worse, you'll eventually run into circular dependency hell when your types start referencing each other across different domain boundaries, and your IDE's "Go to Definition" becomes a trip through a wasteland of unrelated interfaces.
Colocating Types with Domain Logic
The correct approach is Colocation. I want you to stop thinking about "types" as a category of code and start thinking about "features" as the category. If you are building an Order processing system, the types for an Order belong in the Order folder, right next to the functions that process them.
Instead of a global folder, structure your code by domain:
src/features/orders/orders.types.tssrc/features/orders/orders.service.tssrc/features/products/products.types.tssrc/features/products/products.api.ts
Now, when I'm working on the Order service, I only import the types I actually need. My dependency graph stays lean, and if I decide to delete the "Orders" feature entirely, I can just delete that one folder without hunting through a global types.ts file to scrub out orphaned interfaces. It's much more satisfying to delete a folder than to surgically remove lines from a 2,000-line file.
Defining Boundaries with Barrel Files
You might worry that this leads to "import sprawl"—having ten different import lines at the top of your file. This is where Barrel Files (index.ts) come in, but you have to use them with discipline. I use them to define a "Public API" for each feature.
Inside src/features/orders/index.ts, I only export what the rest of the app is allowed to see:
// src/features/orders/index.ts
export { Order, OrderStatus } from './orders.types';
export { createOrder, getOrderById } from './orders.service';
// Note: I am NOT exporting internal helper functions or private types here.
When the Payment feature needs to know about an Order, it imports from the feature folder, not the specific file: import { Order } from '@/features/orders'. This creates a clean boundary. I can change the internal structure of the orders folder—split one file into three, rename a private type—and as long as the index.ts remains the same, I haven't broken a single other part of the application.
📋 Practical Task
Refactoring the Monolithic Shopping Cart Structure
You've inherited a codebase where the previous developer put everything into two giant files: src/types.ts and src/api.ts. Your task is to refactor this into a feature-based structure to improve maintainability and build performance.
Current State:
// src/types.ts
export interface Cart { id: string; items: CartItem[]; }
export interface CartItem { productId: string; qty: number; }
export interface User { id: string; email: string; }
export interface UserProfile { bio: string; avatarUrl: string; }
// src/api.ts
export async function updateCart(cart: Cart) { /* ... */ }
export async function getUser(id: string): Promise<User> { /* ... */ }
export async function updateProfile(profile: UserProfile) { /* ... */ }
Your Goal:
- Create two feature folders:
src/features/cartandsrc/features/user. - Move the related types and functions into their respective folders (e.g.,
cart.types.tsandcart.service.ts). - Implement a barrel file (
index.ts) for each feature that exports only the necessary types and functions. - Verify that you can import
CartandupdateCartfrom@/features/cartwithout importing anything related to the User domain.
There are no comments for now.