Skip to Content
Course content

167: When to Still Use Namespaces

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was tasked with migrating a massive, decade-old internal dashboard to TypeScript. The project relied on a proprietary charting library written back when "modules" meant just dropping five <script> tags in the HTML head in a very specific order. The library didn't export anything; it just dumped a giant ChartEngine object onto the window. Every time I tried to use a standard ES module import, the compiler screamed at me because the library didn't have a package.json or any export statements. I spent two days fighting the build system before I realized I was trying to use a modern tool for a legacy problem.

In modern TypeScript, we almost always use ES modules (import and export). If you're starting a new project today, you'll probably never write the namespace keyword. But namespaces aren't "deprecated" in the sense that they're gone; they just serve a different purpose now. Think of a namespace as a way to group related code under a single name to avoid polluting the global scope, specifically when you aren't in a module-based environment.

Mapping Global Legacy Libraries

The most common reason I still reach for namespaces is when writing declaration files (.d.ts) for libraries that don't use modules. When a library attaches itself to the global window object, a standard export won't work because the library isn't a module—it's a global script.

This is where declare namespace comes in. It tells TypeScript: "Hey, there is a global object with this name, and here is the shape of everything inside it." For that legacy charting library, my solution looked like this:

declare namespace ChartEngine {
    interface Options {
        width: number;
        height: number;
        theme: 'dark' | 'light';
    }

    function render(elementId: string, data: any[], options: Options): void;
    function clearAll(): void;
}

By doing this, I could type ChartEngine.render(...) anywhere in my app without an import statement, and TypeScript knew exactly what the Options interface looked like. It mirrored the reality of the runtime environment perfectly.

Internal Grouping and the Module Trade-off

You might occasionally see namespaces used inside a single file to organize a huge set of related types. For instance, if you have a complex API response with fifty different nested interfaces, wrapping them in a namespace can keep your autocomplete clean. Instead of seeing UserAddress, UserPreferences, and UserSecurity all floating in the top-level scope, you get User.Address, User.Preferences, and so on.

However, I'll give you a word of caution: don't let this become a habit in your application logic. Using namespaces for organization in a project that already uses ES modules is usually an anti-pattern. If your file is getting so large that you need namespaces to organize your types, the real solution is to break that file into smaller, focused modules. Namespaces create a "closed" world that can make tree-shaking harder for bundlers like Webpack or Vite. I've seen projects where the bundle size stayed bloated because the compiler couldn't prove that a specific part of a namespace wasn't being used elsewhere.

Stick to namespaces when you are describing a world you didn't create (like a global legacy library) or when you are working in an environment that absolutely cannot support modules. In every other case, a folder full of small files and explicit imports is the way to go.




📋 Practical Task

Defining Types for the "LegacyAnalytics" Global SDK

Imagine you've just joined a project that uses an ancient analytics SDK. The SDK is loaded via a script tag and creates a global object called LegacyAnalytics. It has a method trackEvent that takes a string event name and an object of properties, and a setUser method that takes a user ID and an email.

Your task is to create a declaration file (or a block of code) using a namespace that describes this global object so that the rest of the team gets full IntelliSense and type checking when calling LegacyAnalytics.trackEvent() and LegacyAnalytics.setUser() without needing to import them.

Requirements:

  • Use declare namespace LegacyAnalytics.
  • Define a trackEvent function that accepts a string and a Record<string, any>.
  • Define a setUser function that accepts a number (userId) and a string (email).
  • Ensure these are declared as part of the global scope.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.