-
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
94: Composite Projects and Incremental Builds
I've noticed a recurring pattern when developers start scaling their TypeScript apps: they try to handle multiple packages or folders by simply adding them all to the include array in a single root tsconfig.json. They think, "If TypeScript can see the files, it knows how they relate."
Here is why that's a trap. When you use a single massive include list, TypeScript treats your entire codebase as one monolithic project. If you have a shared-utils folder and a web-app folder, and you change one line of CSS-in-JS in the web-app, TypeScript might still feel the need to re-verify the type-integrity of your shared-utils because they are part of the same compilation unit. As your project grows to thousands of files, your "quick" type-check becomes a three-minute coffee break.
"Just adding folders to 'include' manages my dependencies" is not the same as Project References
The real way to handle this is through Project References. Instead of one giant bucket, you create smaller, discrete projects—each with its own tsconfig.json—and explicitly tell TypeScript how they depend on each other.
Imagine you have a project structure like this:
/project
├── packages/
│ ├── core/
│ │ ├── tsconfig.json
│ │ └── index.ts
│ └── api/
│ ├── tsconfig.json
│ └── server.ts
└── tsconfig.json (Solution config)
In the core project, you aren't just writing code; you're creating a library that the api project consumes. To make this work, core/tsconfig.json needs "composite": true. This tells TypeScript that this project is a "building block" for others. It forces a few things: you must enable declaration (so TypeScript can generate .d.ts files) and you must specify an outDir.
Then, in api/tsconfig.json, you don't just import the files; you add a reference:
{ "compilerOptions": { "composite": true, "outDir": "../../dist/api" }, "references": [ { "path": "../core" } ] }Using 'incremental' to stop doing the same work twice
Once you've split your project into composites, you'll want to enable
"incremental": true. Whilecompositedefines the structure,incrementalmanages the memory.When you turn this on, TypeScript creates a
.tsbuildinfofile. I like to think of this as a "save state" for the compiler. It stores a graph of your files and their hashes. The next time you run the compiler, TypeScript looks at that file and says, "Okay,core/index.tshasn't changed since the last run, and nothing it depends on has changed either. I'll skip it entirely."But here is the catch: you can't just run
tscanymore. If you do, you're just running the compiler on the current directory, ignoring the reference graph. You have to use the build mode:npm run tsc --buildThe
--build(or-b) flag is the magic sauce. It orchestrates the entire process. It looks at your project references, determines the correct order of compilation (buildingcorebeforeapi), and uses the.tsbuildinfofiles to only compile what is actually dirty. I've seen build times drop from minutes to seconds just by switching from a monolithicincludeto a-bworkflow.
📋 Practical Task
Configure a Multi-Package Build Graph for a Shared Logic Library
You are managing a monorepo with two parts: a shared-logic package and a client-app package. Currently, the build is slow because everything is in one project. Your goal is to convert this into a Composite Project structure.
Requirements:
- Create a
packages/shared-logic/tsconfig.jsonthat enablescompositeanddeclaration, and sets anoutDirto../../dist/shared-logic. - Create a
packages/client-app/tsconfig.jsonthat also enablescompositeandincremental, and contains areferencesarray pointing to theshared-logicfolder. - Create a root
tsconfig.json(the "solution" config) that uses"files": []and"references": [...]to link both theshared-logicandclient-appprojects. - Explain which command you would run from the root directory to trigger an incremental build of the entire dependency graph.
There are no comments for now.