2
votes

In the TypeScript documentation, namespaces are being called as internal TypeScript modules and for using them as reference inside individual files we have to add a ref header to the namespace such as: /// <reference path="Validation.ts" /> as mentioned in the documentation. However, we could also export and declare the namespace with: export declare namespace MyNameSpace and then import it into any other entity. Is there any reason we are being adviced to use header ref tag instead of export of namespace declaration.

After hearing the first answer regarding the order of compilation and runtime, I thought maybe it's better to provide an example, here is a simple greeting app that contains three mains files with declared namespace (which I doubt we even need to import when we want to use it within our application scope)

Filename: GreetingApp.ts

export declare namespace GreetingApp {}

Filename: Hello.ts

namespace GreetingApp {
    export class Hello {
        constructor(private name: string) {
            this.name = name.toLowerCase().charAt(0).toUpperCase() + name.substr(1).toLowerCase();
        }

        public toString(): string {
            return "Hello! " + this.name;
        }
    }
}

Filename: Bye.ts

namespace GreetingApp {
    export class Bye {
        constructor(private name: string) {
            this.name = name.toLowerCase().charAt(0).toUpperCase() + name.substr(1).toLowerCase();
        }

        public toString(): string {
            return "Goodbye! " + this.name;
        }
    }
}

Filename: UseCase.ts (Testfile to import and instantiate the objects)

import GreetingApp from './GreetingApp';

console.log(new GreetingApp.Hello('adam').toString());
console.log(new GreetingApp.Bye('adam').toString());
1
To make sure I'm understanding this correctly, can you please mark which part of the code block corresponds to each file?Matt McCutchen
Sure I updated the question and separated source code for each filenegative0

1 Answers

1
votes

If you don't use export/import, then all your files run in the global scope, and if multiple files declare namespaces with the same name (as in the example you linked), the namespaces get merged. This approach can be very convenient; it's used in the TypeScript compiler source code itself as well as in one of my projects. (The TypeScript team claims that external modules are superior to namespaces, but I'll believe them when they migrate the compiler from namespaces to external modules!) Note that /// <reference path="..."> serves two purposes: loading the declarations from the target file at compile time and ensuring that the target file is loaded before the current file at runtime. You'll be able to leave out the /// <reference path="..."> if you meet both of the following conditions: you use a tsconfig.json file to ensure that all your files are loaded at compile time, and the runtime load order doesn't matter. One of the most common reasons why runtime load order might matter is if you define a class and then extend it in a different file.

If you export and import the namespace, then you are using external modules, and if you declare two namespaces with the same name in different external modules, they won't merge. One external module can contain a module augmentation that adds declarations to a namespace in another external module, but not runtime code. So this is not as convenient as merging namespaces in files that run in the global scope.

Round 2

In UseCase.ts, I'm assuming you intended to import { GreetingApp } from './GreetingApp', because import GreetingApp from './GreetingApp' is giving me a compile error. (If so, please update the question.)

Wow, you have found a crazy aspect of TypeScript name resolution that I was previously unaware of. In this example, Hello.ts and Bye.ts are global files and not external modules (because they contain no top-level ES6 imports or exports), so both contribute to a GreetingApp namespace in the global scope, which has nothing to do with the GreetingApp namespace exported from GreetingApp.ts.

So what is happening in UseCase.ts? As mentioned here, a TypeScript symbol can have one or more of the following meanings: value (including namespace of values), type, and "namespace" (meaning namespace of types). The GreetingApp namespace in GreetingApp.ts has only a namespace meaning because it contains no values, while the global GreetingApp namespace has both a value meaning and a namespace meaning because it contains classes, which consist of both values (constructor functions) and types (instance types).

When a symbol is referenced, the context of the reference determines which of the three meanings is used: if you use X.Y as a value, the value meaning of X is used and its property Y is taken, while if you use X.Y as a type, the namespace meaning of X and the type meaning of Y within X are used. Furthermore, shadowing of symbols works per-meaning! When UseCase.ts imports GreetingApp from ./GreetingApp, it imports the only meaning that the GreetingApp in GreetingApp.ts has, which is a namespace meaning, shadowing the namespace meaning of the global GreetingApp. The value meaning of the global GreetingApp is still visible and is used by the references to GreetingApp.Hello and GreetingApp.Bye. However, you'll notice that if you try to declare a variable of type GreetingApp.Hello, it doesn't work, because that reference uses the namespace meaning of GreetingApp, which resolves to the empty namespace from GreetingApp.ts and not the global namespace.

I know this analysis is pretty technical, but I hope I've made clear that the example isn't working the way you probably thought it was (and I hope the next person confused by TypeScript's underdocumented shadowing behavior will find this thread in a web search). The import and the entire GreetingApp.ts file are serving no purpose in the example. If you delete both of them, you are left with the "global files with merged namespaces" approach to structuring your codebase that I described in the original answer.