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
169: Project References for Large Codebases
Once your TypeScript project grows past a few dozen files, you'll start noticing that tsc is taking longer and longer to run. The compiler is essentially re-checking the entire world every time you change a single line of code. In a professional environment, this kills productivity. Project References are the solution to this; they allow us to break a monolithic codebase into smaller, independent pieces that can be compiled incrementally.
Structuring a core library and a client app
Let's imagine we're building a system where we have a shared core package (containing our business logic and types) and a client package (the actual application). I want the client to depend on core, but I don't want the compiler to re-analyze core every time I tweak a CSS-in-JS style in the client.
First, I'll set up my folder structure like this:
/my-project
├── core/
│ ├── tsconfig.json
│ └── index.ts
└── client/
├── tsconfig.json
└── main.ts
In core/index.ts, I'll put a simple data model:
export interface User {
id: string;
username: string;
}
export const formatUser = (user: User) => `User: ${user.username}`;
Configuring the dependency boundary
Now, here is where the magic happens. To make a project "referenceable," it needs a specific flag in its tsconfig.json. I'll set up the core config first:
// core/tsconfig.json
{
"compilerOptions": {
"outDir": "../dist/core",
"rootDir": ".",
"composite": true,
"declaration": true
}
}
The composite: true flag is the key here. It tells TypeScript that this project is intended to be referenced by others, which forces it to generate .d.ts files (declarations) so that the consuming project doesn't have to re-compile the source code of the library.
The "Missing Reference" mistake
Now I'll set up the client. I'll create client/main.ts to use that formatUser function:
import { User, formatUser } from "../core/index";
const me: User = { id: "1", username: "dev_pro" };
console.log(formatUser(me));
And the client/tsconfig.json:
{
"compilerOptions": {
"outDir": "../dist/client",
"rootDir": ".",
},
"references": [
{ "path": "../core" }
]
}
I'll try to run tsc in the client folder. But wait—I get an error. TypeScript is complaining that it can't find the declaration files for core. I realized I made a classic mistake: I'm trying to compile the client, but I haven't actually built the core project yet. Project references don't magically compile dependencies on the fly during a standard tsc call; they rely on the dependency already being built into its outDir.
I could manually run tsc in the core folder first, but that's tedious. Instead, I'll use the build mode flag.
Using the Build Mode for incremental success
Instead of running tsc, I'll use tsc --build (or tsc -b) from the root of the project. This is the "smart" way to handle referenced projects.
# From the project root
tsc -b client
When I run this, TypeScript looks at the references array in the client config, sees that it depends on core, checks if core has changed, builds core if necessary, and then builds client. It's a dependency graph in action.
If I change a file in client and run tsc -b client again, you'll notice it's nearly instantaneous. TypeScript knows core hasn't changed, so it skips it entirely. This is how you keep a massive codebase from feeling like a slog.
📋 Practical Task
Exercise: Splitting a Shared Utility Library from a Web Application
You have a project with a shared utility folder and a main application folder. Your goal is to implement Project References to ensure the utility library is compiled independently.
Requirements:
- Create a folder named
utilswith atsconfig.json. Ensure it is configured as a composite project that outputs to../dist/utils. - Create a file
utils/math.tsthat exports a functionadd(a: number, b: number): number. - Create a folder named
appwith atsconfig.json. Configure it to reference theutilsproject. - Create a file
app/index.tsthat importsaddfrom../utils/mathand logs the result ofadd(5, 10). - Verify your setup by running
tsc -b appfrom the root directory and ensure thedistfolder contains the compiled output for both projects.
There are no comments for now.