-
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
155: How TypeScript Models JavaScript Modules
A few years ago, I was onboarding a junior dev who had spent a good three hours scratching their head over a "Cannot redeclare block-scoped variable" error. They had two separate files—user-service.ts and auth-service.ts—and both had a top-level constant named API_URL. They were baffled. In their mind, these were two different files, so they should have two different scopes. But TypeScript was screaming that API_URL had already been defined.
The problem wasn't a bug in the compiler; it was a fundamental misunderstanding of how TypeScript models the JavaScript module system. They were treating their files like isolated silos, but TypeScript was treating them as one giant, global script.
The invisible line between Scripts and Modules
In TypeScript, there is a massive distinction between a "script" and a "module." If a file doesn't contain any import or export statements, TypeScript treats it as a script. This means every variable, function, or interface declared at the top level is dumped into the global namespace. This is why our junior dev was seeing that error; both files were scripts, so they were sharing the same global bucket.
The moment you add a single export {} (even an empty one) or import something from another file, TypeScript flips a switch. That file is now a module. It gets its own isolated scope. Variables declared inside are no longer global; they are private to that module unless explicitly exported. I usually tell people to get into the habit of treating every single file as a module from day one. It saves you from the nightmare of naming collisions as your project grows from five files to five hundred.
// This is a script. 'config' is global.
const config = { port: 8080 };
// This is a module. 'config' is local to this file.
export const config = { port: 8080 };
How TypeScript maps the module graph
When you write import { UserService } from './services/user', TypeScript doesn't just blindly look for a file. It performs a resolution process. It looks for user.ts, then user.tsx, then maybe a user.d.ts. This is where things can get hairy if your tsconfig.json isn't aligned with your runtime.
If you're targeting Node.js, you might be using CommonJS (require), but if you're targeting the browser or modern Node, you're likely using ESM (import/export). TypeScript models both, but the moduleResolution setting in your config tells the compiler how to behave. If you set it to node (or node16/nodenext), TypeScript mimics the way Node.js searches for packages in node_modules. If you've ever seen a "Module not found" error despite the file being right there, it's almost always a mismatch between how you're importing the file and how the resolver is configured to find it.
Modeling external code with Ambient Declarations
Now, not every module is something you wrote. Sometimes you're using a legacy JavaScript library that doesn't have types. TypeScript models these using "ambient declarations"—those .d.ts files. These files don't generate any JavaScript; they are purely a map for the compiler.
When you install @types/lodash, you're essentially giving TypeScript a blueprint of that module's shape. It tells the compiler, "Trust me, when the code runs, a module named 'lodash' will exist, and it will have a function called chunk that takes these specific arguments." This allows TypeScript to model the module's interface without ever needing to see the actual implementation code. It's a layer of abstraction that lets the type system stay decoupled from the actual JS execution.
📋 Practical Task
Fixing the Global Scope Leak in a Config Project
You have been handed a small project with two files that are causing a naming collision because they are being treated as scripts rather than modules. Your task is to isolate their scopes so the project compiles without errors.
File 1: database.ts
const connectionString = "mongodb://localhost:27017";
function connect() {
console.log("Connecting to " + connectionString);
}
File 2: cache.ts
const connectionString = "redis://localhost:6379";
function connect() {
console.log("Connecting to " + connectionString);
}
The Goal:
- Modify both files so that they are treated as modules rather than scripts.
- Ensure that
connectionStringandconnectcan exist in both files without a "Cannot redeclare" error. - Export the
connectfunction from both files so they could theoretically be used elsewhere.
There are no comments for now.