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
166: Namespaces vs Modules
If you've been digging through older TypeScript projects or some specific legacy libraries, you've probably run into namespace. It looks clean, it feels organized, and for a while, it was the way we handled "internal" organization. But in modern TypeScript, the module system (ES Modules) has almost entirely won. I want to show you why by building a tiny combat system for a game.
Tidying up Game Logic with Namespaces
Imagine we're building a game. We have combat logic and UI logic. To keep them from clashing in the global scope, I might start by using namespaces. This essentially wraps our code in a local object.
namespace Game.Combat {
export class Fighter {
constructor(public name: string, public hp: number) {}
attack() {
console.log(`${this.name} attacks!`);
}
}
export function calculateDamage(base: number) {
return base * Math.random();
}
}
const hero = new Game.Combat.Fighter("Alaric", 100);
hero.attack();
This feels intuitive. I've created a "folder" for my code called Game.Combat. Everything inside is tucked away, and I access it via that dot notation. It solves the immediate problem of naming collisions, but it creates a bigger one: everything is still technically hanging off the global window (or global object) once it's compiled to JavaScript.
The Global Scope Headache
The problem with namespaces is that they don't tell the compiler—or other developers—exactly where a piece of code comes from. If I have five different files all contributing to the Game.Combat namespace, I have to make sure they are loaded in the exact right order in my HTML script tags. If CombatUtils.ts loads after Fighter.ts, but Fighter depends on a utility function, the whole thing crashes at runtime.
This is why we moved to modules. Modules treat every file as its own isolated world. Nothing leaks out unless you explicitly export it, and nothing comes in unless you explicitly import it.
Moving to the Modern Standard: Modules
Let's refactor that combat system. Instead of one big namespace, I'll split the logic into two files. This is how you'll spend 99% of your time in professional TypeScript development.
First, combat-utils.ts:
export function calculateDamage(base: number) {
return base * Math.random();
}
Then, fighter.ts:
import { calculateDamage } from './combat-utils';
export class Fighter {
constructor(public name: string, public hp: number) {}
attack() {
const dmg = calculateDamage(10);
console.log(`${this.name} deals ${dmg.toFixed(2)} damage!`);
}
}
The 'Missing Import' Moment
Here is where I actually tripped up when I first switched from namespaces to modules. I wrote a main entry file, and because I was still thinking in "namespace mode," I assumed that since Fighter was exported, it was just... available.
// main.ts
// I tried to just do this:
const hero = new Fighter("Alaric", 100); // Error: Cannot find name 'Fighter'.
hero.attack();
I spent a good two minutes wondering why my compiler was suddenly "broken." The reality is that modules are strictly isolated. In a namespace, Game.Combat.Fighter was a global variable. In a module, Fighter doesn't exist in main.ts until I explicitly ask for it. I had to go back to the top and add import { Fighter } from './fighter';.
It seems like a chore to write imports everywhere, but it's a lifesaver. When you open a file in a massive project, you can look at the top and see exactly every single dependency that file has. No more guessing which script tag order is causing the crash.
📋 Practical Task
Refactoring the Legacy WalletNamespace to ESM
You've inherited a legacy file that uses namespaces, and your team wants to move it to a modern module-based architecture. Your goal is to split the following code into two separate files: currency-converter.ts and wallet.ts.
// Current legacy code (single file)
namespace Finance.Wallet {
export function convertToUSD(amount: number, rate: number) {
return amount * rate;
}
export class Wallet {
constructor(public balance: number) {}
addFunds(amount: number) {
this.balance += amount;
}
}
}
Requirements:
- Create
currency-converter.tscontaining theconvertToUSDfunction as a named export. - Create
wallet.tscontaining theWalletclass as a named export. - Inside
wallet.ts, importconvertToUSDand add a new method to theWalletclass calledgetBalanceInUSD(rate: number)that uses the imported converter function to return the balance converted to USD. - Ensure no
namespacekeywords remain in your final code.
There are no comments for now.