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.