Skip to main content

Namespaces_TypeScript Notes 15

Free2019-04-13#TypeScript#TypeScript namespace vs module#TypeScript module keyword#TypeScript /// <reference#TypeScript internal module vs external module#TypeScript内部模块与外部模块

Namespaces originate from the module pattern in JavaScript, another way of organizing code

I. Origin

Namespaces originate from the module pattern in JavaScript:

var MyModule = {};
(function(exports) {
  // Private variables
  var s = "hello";
  // Public functions
  function f() {
    return s;
  }
  exports.f = f;
})(MyModule);

MyModule.f();
// Error MyModule.s is not a function
MyModule.s();

Composed of two parts:

  • Module closure: Encapsulates module implementation, isolates scope
  • Module object: Variables and functions exposed by the module

Later expanded on this basis with module dynamic loading, splitting into multiple files and other support

TypeScript combined module pattern and class pattern to implement a module mechanism, i.e. namespaces:

namespace MyModule {
  var s = "hello";
  export function f() {
    return s;
  }
}

MyModule.f();
// Error Property 's' does not exist on type 'typeof MyModule'.
MyModule.s;

Compilation product is the classic module pattern:

var MyModule;
(function (MyModule) {
  var s = "hello";
  function f() {
    return s;
  }
  MyModule.f = f;
})(MyModule || (MyModule = {}));
MyModule.f();
MyModule.s;

II. Function

Similar to modules, namespaces are also a way of organizing code:

namespace Page {
  export interface IPage {
    render(data: object): string;
  }
  export class IndexPage implements IPage {
    render(data: object): string {
      return '<div>Index page content is here.</div>';
    }
  }
}

// Compilation result
var Page;
(function(Page) {
  var IndexPage = /** @class */ (function() {
    function IndexPage() {}
    IndexPage.prototype.render = function(data) {
      return '<div>Index page content is here.</div>';
    };
    return IndexPage;
  })();
  Page.IndexPage = IndexPage;
})(Page || (Page = {}));

Similarly has scope isolation (above example only exposes Page one global variable), also supports splitting modules by files:

// IPage.ts
namespace Page {
  export interface IPage {
    render(data: object): string;
  }
}

// IndexPage.ts
/// <reference path="./IPage.ts" />
namespace Page {
  export class IndexPage implements IPage {
    render(data: object): string {
      return '<div>Index page content is here.</div>';
    }
  }
}

// App.ts
/// <reference path="./IndexPage.ts" />
/// <reference path="./DetailPage.ts" />
let indexPageContent = new Page.IndexPage().render({value: 'index'})

Compilation result is:

// IPage.js
Empty

// IndexPage.js
/// <reference path="./IPage.ts" />
var Page;
(function (Page) {
    var IndexPage = /** @class */ (function () {
        function IndexPage() {
        }
        IndexPage.prototype.render = function (data) {
            return '<div>Index page content is here.</div>';
        };
        return IndexPage;
    }());
    Page.IndexPage = IndexPage;
})(Page || (Page = {}));


// App.js
/// <reference path="./IndexPage.ts" />
/// <reference path="./DetailPage.ts" />
var indexPageContent = new Page.IndexPage().render({ value: 'index' });

Notice here uses triple-slash directives to introduce split-out "namespace modules" (not like module's import), still using import will get error:

// Error File '/path/to/IndexPage.ts' is not a module.ts(2306)
import IndexPage from "./IndexPage";

P.S. Additionally, can use --outFile option to generate a JS Bundle (default compilation generates corresponding same-name JS scattered files)

III. Triple-Slash Directives

Supports 6 types of directives:

  • Describe file dependencies: /// <reference path="./myFile.ts" />, references myFile.ts under current directory
  • Describe (type) declaration dependencies: /// <reference types="node" />, references @types/node/index.d.ts type declarations, corresponds to --types option
  • Explicitly reference built-in (type) library files: /// <reference lib="es2015" /> or /// <reference lib="es2017.string" />, references built-in (type) library lib.es2015.d.ts or lib.es2017.string.d.ts, corresponds to --lib compilation option
  • Disable default library: /// <reference no-default-lib="true"/>, doesn't load default library during compilation process, corresponds to --noLib compilation option, simultaneously marks current file as default library (so --skipDefaultLibCheck option can skip checking this file)
  • Specify current module's AMD module name: ///<amd-module name="NamedModule"/>, specifies AMD module name as NamedModule
  • Specify AMD module dependencies (deprecated): /// <amd-dependency path="legacy/moduleA" name="moduleA"/>, depends on legacy/moduleA, and specifies imported module name as moduleA (name attribute is optional)

P.S. More examples, see Triple-Slash Directives

Formally starts with 3 slashes, therefore called triple-slash directives, where XML tags are used to express compilation directives. Other notes as follows:

  • Must appear at file's first line (except comments) to be effective. That is to say, before a triple-slash directive can only appear single-line comments, multi-line comments or other triple-slash directives
  • /// <amd-dependency /> directive is deprecated, use import "moduleName"; instead
  • When specifying --noResolve option, ignores all /// <reference path="..." /> directives (doesn't import these modules)

In function, /// <reference path="..." /> is similar to @import in CSS (when specifying --outFile option, module integration order is consistent with path reference directive order)

In implementation, during preprocessing phase will depth-first parse all triple-slash directives, adding specified files to compilation process

P.S. Triple-slash directives appearing in other positions will be treated as ordinary single-line comments, no error, but ineffective (compiler doesn't recognize)

IV. Aliases

Namespaces support nesting, therefore may appear deep nesting situations:

namespace Shapes {
  export namespace Polygons {
    export class Triangle { }
    export class Square { }
  }
}

At this time can use aliases to simplify module references:

import P = Shapes.Polygons;
import Triangle = Shapes.Polygons.Triangle;
let sq = new P.Square();
let triangle = new Triangle();

// After compilation
var P = Shapes.Polygons;
var Triangle = Shapes.Polygons.Triangle;
var sq = new P.Square();
var triangle = new Triangle();

Not hard to discover, here import is var's syntax sugar:

This is similar to using var, but also works on the type and namespace meanings of the imported symbol.

Therefore when aliasing a value will create a new reference:

Importantly, for values, import is a distinct reference from the original symbol, so changes to an aliased var will not be reflected in the original variable.

For example:

namespace NS {
  export let x = 1;
}
import x = NS.x;
import y = NS.x;
(x as any) = 2;
y === 1;    // true

// After compilation
var NS;
(function (NS) {
    NS.x = 1;
})(NS || (NS = {}));
var x = NS.x;
var y = NS.x;
x = 2;
y === 1; // true

P.S. import q = x.y.z alias syntax only applies to namespaces, requires right side must be namespace access

V. namespace and module

Before TypeScript 1.5 there was only module keyword, didn't distinguish internal modules (internal modules) and external modules (external modules), both were defined through module keyword. Later for clarity, added namespace keyword to represent internal modules

(Excerpted from namespace keyword)

In short, keywords module and namespace are completely equivalent in syntax, for example:

namespace Shape.Rectangle {
  export let a;
  export let b;
  export function getArea() { return a * b; }
}

And

module Shape.Rectangle {
  export let a;
  export let b;
  export function getArea() { return a * b; }
}

Compilation results are both:

var Shape;
(function (Shape) {
  var Rectangle;
  (function (Rectangle) {
    function getArea() { return Rectangle.a * Rectangle.b; }
    Rectangle.getArea = getArea;
  })(Rectangle = Shape.Rectangle || (Shape.Rectangle = {}));
})(Shape || (Shape = {}));

Using namespace to replace old module is just to avoid confusion, or say to make way for ES Module, AMD, UMD and other Module concepts (so-called external modules). Because if occupying module keyword, but actually defining not Module but Namespace, it's a very confusing thing

VI. Modules and Namespaces

Internal Modules and External Modules

That is to say:

  • Internal modules: i.e. namespaces, declared through namespace or module keywords
  • External modules: i.e. modules (such as ES Module, CommonJS, AMD, UMD, etc.), no need to explicitly declare, (containing import or export) files are modules

External modules can simply understand as modules in external files, because can define multiple different namespace or module (i.e. internal modules) in same file, but cannot define multiple ES Modules

P.S. After all namespaces are essentially IIFE, unrelated to module loaders, doesn't exist file-is-module loading mechanism constraints

Concept Differences

Conceptually, TypeScript follows ES Module specification (file is module), outputs CommonJS, AMD, UMD and other module forms through compilation

While namespaces originate from module pattern in JavaScript, considered a product of old era, not recommended to use (except for declaring module types)

Loading Mechanism Differences

In module import mechanism, namespaces need to be imported through triple-slash directives, equivalent to source code embedding (similar to @import in CSS), will introduce extra variables into current scope

P.S. If not bundling into single file Bundle, need to introduce these dependencies imported through triple-slash directives at runtime (for example through <script> tags)

While modules are imported through import/require etc. methods, decided by caller whether to reference it through variables, won't actively affect current scope

P.S. import "module-name"; syntax only imports module (its side effects), doesn't reference and access module, specifically see import

Best Practices

In module and namespace usage, there are some practical experiences:

  • Reduced namespace nesting levels, for example classes containing only static methods are usually unnecessary, module names are enough to express semantics
  • When module only exposes one API, using export default is more suitable, importing is more convenient, and caller doesn't need to care about API name
  • When wanting to expose multiple APIs, directly export all (if too many, import module object, such as import * as largeModule from 'SoLargeModule')
  • Extend existing modules through re-export, for example export as
  • Don't use namespaces in modules, because modules already have logical structure (file directory structure) and module scope, namespaces don't provide more benefits

Reference Materials

Comments

No comments yet. Be the first to share your thoughts.

Leave a comment