Execute Program

Everyday TypeScript: Namespaces

Welcome to the Namespaces lesson!

This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!

  • TypeScript supports the module system in modern JavaScript, sometimes called "ES6 modules". In this system, each source file is a self-contained module. A module can't see variables, functions, types, or other properties defined in other modules. However, a module can choose to "export" certain properties, which can then be "imported" and used by other modules.

  • (This course doesn't cover modules directly. That would break our simple paradigm of self-contained code examples. Fortunately, the module system isn't the hard part of learning TypeScript, because it's the same module system that we know from JavaScript. The hard part of TypeScript is learning all of the type features!)

  • In addition to modules, TypeScript also has "namespaces". These are specific to TypeScript and don't exist in JavaScript.

  • We can think of namespaces as separate "modules" that live side by side inside of a single source file. The language designers' intent was that we'd use modules in some situations and namespaces in others. For example, we might want dozens of very small, related modules. Rather than create dozens of small files, we can use namespaces to put all of them in a single file. In other cases, we'll have large modules that deserve their own files.

  • Namespaces can contain functions, variables defined with const and let, type declarations, etc. To access a namespace's properties, we qualify their names with the namespace name, like Util.wordCount(...).

  • Here's a short example with two namespaces. First, we have a Util namespace: the inevitable junk drawer that shows up in every application. It defines one short function, wordCount. Second, we have a Tests namespace that holds the unit tests.

  • >
    namespace Util {
    export function wordCount(s: string) {
    return s.split(/\b\w+\b/g).length - 1;
    }
    }

    namespace Tests {
    export function testWordCount() {
    if (Util.wordCount('hello there') !== 2) {
    throw new Error("Expected word count for 'hello there' to be 2");
    }
    return 'ok';
    }
    }

    Tests.testWordCount();
    Result:
    'ok'Pass Icon
  • Now we'll intentionally break the code in two different ways.

  • First, we'll try to call testWordCount() without the namespace qualification. That's a type error. This code may all be in one file, but the namespaces are separate. We can only access namespace properties by qualifying them with the namespace's name, like Tests.testWordCount.

  • >
    namespace Util {
    export function wordCount(s: string) {
    return s.split(/\b\w+\b/g).length - 1;
    }
    }

    namespace Tests {
    export function testWordCount() {
    if (Util.wordCount('hello there') !== 2) {
    throw new Error("Expected word count for 'hello there' to be 2");
    }
    return 'ok';
    }
    }

    testWordCount();
    Result:
    type error: Cannot find name 'testWordCount'.Pass Icon
  • Second, we'll remove the export on our wordCount function. That causes a type error when testWordCount tries to call wordCount. Properties, including functions, are only accessible when the namespace exports them. Properties that aren't exported are invisible from outside the namespace. It's as if they don't exist at all.

  • >
    namespace Util {
    function wordCount(s: string) {
    return s.split(/\b\w+\b/g).length - 1;
    }

    // We reference `wordCount` here to prevent this error:
    // type error: 'wordCount' is declared but its value is never read.
    wordCount;
    }

    namespace Tests {
    export function testWordCount() {
    if (Util.wordCount('hello there') !== 2) {
    throw new Error("Expected word count for 'hello there' to be 2");
    }
    return 'ok';
    }
    }

    Tests.testWordCount();
    Result:
    type error: Property 'wordCount' does not exist on type 'typeof Util'.Pass Icon
  • The error message there is a bit weird: it references the typeof type operator, which you may not have seen yet. But the error's meaning is still clear. From outside of the Util module, there is no wordCount; it's invisible.

  • Namespaces can cause some problems in practice. In an earlier lesson, we saw TypeScript's "type-level extension" rule. Normally, the compiler removes all of the type annotations, and what's left is valid JavaScript code.

  • Fortunately, most of TypeScript's design follows that rule. When the compiler needs to generate JavaScript code from const n: number = 1, it can remove the : number part. That leaves const n = 1, which is legal JavaScript.

  • We've seen that enums break the type-level extension rule. The compiler can't delete enums before generating JavaScript code. Instead, it must transform enum type definitions into new JavaScript code that doesn't exist in the original TypeScript source file.

  • Namespaces break the type-level extension rule in the same way as enums. In namespace Util { export function wordCount ... }, we can't remove the type definitions. The entire namespace is a type definition! And what about the other code outside of the namespace calling Util.wordCount(...)? If we delete the Util namespace before generating JavaScript code, then Util doesn't exist any more, so the Util.wordCount(...) function call can't possibly work.

  • For both enums and namespaces, the TypeScript compiler can't delete the type annotations. Instead, it has to generate new JavaScript code that doesn't exist in the original TypeScript code.

  • Now let's consider how the type-level extension rule interacts with the ecosystem of JavaScript and TypeScript tools. TypeScript projects are inherently JavaScript projects, so they tend to use JavaScript build tools like Babel and webpack. These tools were designed for JavaScript, which is still their primary focus today. Each tool is also an ecosystem of its own. In particular, there's a seemingly-endless universe of Babel plugins and webpack plugins, all of which exist to process code.

  • How can Babel, webpack, their many plugins, and all of the other tools in the ecosystem fully support TypeScript? For most of the TypeScript language, the type-level extension rule makes these tools' jobs relatively easy. They strip out the type annotations, leaving valid JavaScript.

  • When it comes to enums and namespaces, things are more difficult. The tools have to implement parts of an actual TypeScript compiler. It's not good enough to remove the enums or namespaces; they have to know how to turn namespace Util { ... } into working JavaScript code, even though JavaScript doesn't have namespaces at all.

  • This brings us to the practical problem with TypeScript's violations of its own type-level extension rule. Tools like Babel and webpack are designed for JavaScript, so TypeScript support is just one feature out of many. Sometimes, TypeScript support doesn't receive as much attention as JavaScript support, which can lead to bugs.

  • The vast majority of tools will do a good job with variable declarations, function definitions, etc.; all of those are relatively easy to work with. But sometimes mistakes creep in with enums and namespaces, because they require more than just stripping off the type annotations. You can trust the TypeScript compiler itself to compile those features correctly, but some rarely-used tools in the ecosystem may make mistakes.

  • For enums, our suggestion was to use unions instead. Our suggestion for namespaces is similar: use regular modules instead. It may be a bit annoying to create many small files, but modules have the same fundamental functionality as namespaces, without the potential downsides.

  • Despite their downsides, you'll probably encounter namespaces in real code, especially in older code bases. Like enums, it's good to have a basic familiarity so you can work with that code.