Employing Namespaces in TypeScript to encapsulate your data

In this lesson, we are going to learn about the precursor of the ECMAScript module implemented purely in TypeScript. These are called namespaces and they are quite fascinating.

Employing Namespaces in TypeScript to encapsulate your data

In the previous lesson, we learn about the standard module system used by TypeScript which also coincides with the standard module system of JavaScript standardized by the ECMAScript.

When the module compiler-option (or the --module flag) is set to CommonJS, the TypeScript compiler converts the ECMAScript module statements such as import and export to equivalent require and exports statements for Node. Else, it is left intact so that it can work inside browsers that support native ECMAScript modules.

The benefit of using a module system in a project is that you can split reusable logic and application logic between multiple files. However, this also means your runtime environment must support one of these module systems.

If you want the compiled JavaScript program to run on Node, you can set the module compiler-option to CommonJS. If you want to run the program in a browser environment, you can use ES2015 or ES2020 value. However, you can’t really achieve isomorphic JavaScript that can run both on the Node and inside a browser at the moment using one of these module systems.

When a standard module system is not required or can’t be implemented but we still want to add some modularity to our project, namespaces are a way to go. Namespaces are a TypeScript feature that compiles to pure JavaScript without require or import statements in the output code.

Since they do not use a platform-dependent module system and they compile to vanilla JavaScript that we are used to since the stone age, they are called internal modules. Let’s dive into it.


What are Namespaces?

You might be familiar with namespaces in programming languages like C++ or Java. A namespace is like a region out of which things cannot escape. The namespace keyword in TypeScript creates such a region.

// a.ts
var _version = "1.0.0";
function getVersion() {
  return _version;
}

console.log(_version); // 1.0.0
console.log(getVersion()); // 1.0.0

In this example, the _version variable is accessible by everyone which probably should have been only accessible in the getVersion function. Also, we do not want getVersion function to be in the global scope since a function with the same name could’ve been added by a third-party library in the global scope. We need to add some encapsulation over these values.

// a.ts
namespace MyLibA {
  const _version = "1.0.0";

  function getVersion() {
    return _version;
  }
}

console.log(_version); // ❌ ERROR
console.log(getVersion()); // ❌ ERROR

In this modification, we wrap the application logic inside the MyLibA namespace. The namespace { ... } block creates a scope, so values inside it do not pollute the global scope.

$ tsc a.ts
a.ts:9:14 - error TS2304: Cannot find name '_version'.
    console.log( _version ); // 1.0.0
                 ~~~~~~~~
a.ts:10:14 - error TS2304: Cannot find name 'getVersion'
    console.log( getVersion() ); // 1.0.0
                 ~~~~~~~~~~

If we try to compile the program, the TypeScript compiler will not allow it since the _version and getVersion values are not defined in the global scope. To get access to them, we need to access them from the namespace.

namespace MyLibA {
  const _version = "1.0.0";

  export function getVersion() {
    return _version;
  }
}

console.log(MyLibA._version); // ❌ ERROR
console.log(MyLibA.getVersion()); // 1.0.0

In this example, we have added export keyword before the getVersion function to make it publicly accessible from the namespace. However, the _version value is not exported, so it won’t be accessible on the namespace.

To access a value exported from the namespace, we use <ns>.<value> expression. The MyLibA.getVersion returns the getVersion function since it was exported from the MyLibA namespace but MyLibA._version won’t be accessible since it was not exported.

To export a value from a namespace, put export before the declaration. This works for variables, classes, functions, enums, and more.

namespace <name> {
  const private = 1;
    function privateFunc() { ... };
    export const public = 2;
    export function publicFunc() { ... };
}

So the time for the ultimate reveal. What do namespaces look like in the compiled javascript code? The answer should be obvious. Since we access public values of a namespace using <ns>.<value> expression, the ns most probably should be an object.

// a.js
var MyLibA;
(function (MyLibA) {
  var _version = "1.0.0";

  function getVersion() {
    return _version;
  }

  MyLibA.getVersion = getVersion;
})(MyLibA || (MyLibA = {}));

console.log(MyLibA.getVersion()); // 1.0.0

When we compile the a.ts program, we get this output. Every namespace in a TypeScript program produces an empty variable declaration (with the same name as the namespace) and an IIFE in the compile JavaScript.

The IIFE contains the code written inside a namespace, therefore the values do not pollute the global scope since they are scoped to the function. The export statements inside a namespace are converted to the property assignation statements as shown here.

MyLibA.getVersion = getVersion;

This makes getVersion value inside the IIFE available on the MyLibA global object and therefore anyone can access it. In contrast to that, the _version value isn’t accessible outside the IIFE.

Exporting Types and Namespaces

Like a module, you can also export types from a namespace.

// a.ts
namespace MyLibA {
  export interface Person {
    name: string;
    age: number;
  }

  export function getPerson(name: string, age: number): Person {
    return { name, age };
  }
}

const ross: MyLibA.Person = MyLibA.getPerson("Ross", 30);

In this example, we are exporting the Person interface from the MyLibA namespace, therefore we can use MyLibA.Person in a type annotation expression. However, since MyLibA.Person is a type, it won’t exist in the compiled JavaScript code.

// a.js
var MyLibA;
(function (MyLibA) {
  function getPerson(name, age) {
    return { name: name, age: age };
  }

  MyLibA.getPerson = getPerson;
})(MyLibA || (MyLibA = {}));

var ross = MyLibA.getPerson("Ross", 30);

Since a namespace is also a value, you can also export a namespace from within a namespace. These are called nested namespaces.

// a.ts
namespace MyLibA {
  export namespace Types {
    export interface Person {
      name: string;
      age: number;
    }
  }

  export namespace Functions {
    export function getPerson(name: string, age: number): Types.Person {
      return { name, age };
    }
  }
}
const ross: MyLibA.Types.Person = MyLibA.Functions.getPerson("Ross Geller", 30);

In this example, MyLibA exports two namespaces: Types and Functions. Namespaces are lexically scoped, so getPerson can access Types.Person from the outer scope.

// a.js
var MyLibA;
(function (MyLibA) {
  var Functions;
  (function (Functions) {
    function getPerson(name, age) {
      return { name: name, age: age };
    }
    Functions.getPerson = getPerson;
  })((Functions = MyLibA.Functions || (MyLibA.Functions = {})));
})(MyLibA || (MyLibA = {}));

var ross = MyLibA.Functions.getPerson("Ross Geller", 30);

Aliasing

The previous example shows how nested namespaces can become noisy quickly. MyLibA.Functions.getPerson is a lot to type, so we can create a shorter reference.

// a.ts
namespace MyLibA {
  export namespace Types {
    export interface Person {
      name: string;
      age: number;
    }
  }

  export namespace Functions {
    export function getPerson(name: string, age: number): Types.Person {
      return { name, age };
    }
  }
}

var Person = MyLibA.Types.Person; // ❌ ERROR
var API = MyLibA.Functions;
const ross: Person = API.getPerson("Ross Geller", 30);

In this example, we save MyLibA.Functions into a shorter API variable. The same trick does not work for Person, because MyLibA.Types.Person is a type, not a runtime value. We could use type Person = MyLibA.Types.Person for that.

But TypeScript provides an easier syntax to create aliases for namespaces that works well with both exported types and values. Instead of var <alias> =, we need to use import <alias> = expression.

// a.ts
namespace MyLibA {
  export namespace Types {
    export interface Person {
      name: string;
      age: number;
    }
  }

  export namespace Functions {
    export function getPerson(name: string, age: number): Types.Person {
      return { name, age };
    }
  }
}

import Person = MyLibA.Types.Person;
import API = MyLibA.Functions;
const ross: Person = API.getPerson("Ross Geller", 30);

In this example, we have just changed the var <alias> declarations to import <alias> expression. This shouldn’t be compared with ES6 import statement. This is just a syntactical sugar to create an alias for namespaces.

// a.js
var MyLibA;
(function (MyLibA) {
  var Functions;

  (function (Functions) {
    function getPerson(name, age) {
      return { name: name, age: age };
    }
    Functions.getPerson = getPerson;
  })((Functions = MyLibA.Functions || (MyLibA.Functions = {})));
})(MyLibA || (MyLibA = {}));

var API = MyLibA.Functions;
var ross = API.getPerson("Ross Geller", 30);

As you can see from this output, aliasing a namespace export using import creates a variable that references the exported value. If an alias references a type, it is simply ignored in the compiled output.

Importing Namespaces

To separate the application logic from reusable logic, we normally create different files and place them in separate directories. In this example, we place namespace declarations in a separate file and import them using an import statement. Since namespaces are regular values, we can import them.

// a.ts
import { MyLibA } from "./b";
const ross: MyLibA.Person = MyLibA.getPerson("Ross Geller", 30);

// b.ts
export namespace MyLibA {
  export interface Person {
    name: string;
    age: number;
  }

  export function getPerson(name: string, age: number): Person {
    return { name, age };
  }
}

The problem with this again is that we are becoming platform-dependent since the compiled JavaScript would need a module system at runtime as shown in this output for the CommonJS module system.

// a.js
var b_1 = require("./b");
var ross = b_1.MyLibA.getPerson("Ross Geller", 30);

// b.js
var MyLibA;
(function (MyLibA) {
  function getPerson(name, age) {
    return { name: name, age: age };
  }
  MyLibA.getPerson = getPerson;
})((MyLibA = exports.MyLibA || (exports.MyLibA = {})));

Since the b.ts file is already imported inside a.ts, you can simply use the tsc —module CommonJS a.ts command to compile this project and TypeScript compiler will automatically include b.ts in the compilation process.

Modularization

TypeScript provides triple-slash directives which are nothing but JavaScript comments that help the TypeScript compiler to locate other TypeScript files and include them in the compilation process.

/// <reference path="./b.ts" />

You can compare these with the preprocessor directives used in C and C++ language such as #include "stdio.h". These must appear at the top of the file to attain special meaning. Since these directives are just comments, their job only exists at the compile-time.

The path attribute of this reference directive points to another TypeScript file and creates a dependency. This is similar to importing b.ts, but without listing imported members.

When a reference of another TypeScript file is given using the reference directive, TypeScript automatically includes that files in the compilation process much like the import statement. All the global values of that file become available in the file where it was referenced.

// a.ts
/// <reference path="./b.ts"/>
const ross: MyLibA.Person = MyLibA.getPerson("Ross Geller", 30);

// b.ts
namespace MyLibA {
  export interface Person {
    name: string;
    age: number;
  }

  export function getPerson(name: string, age: number): Person {
    return { name, age };
  }
}

In this example, we have referenced b.ts inside a.ts using reference directive. Using this, all the values inside b.ts that are in the global scope will be accessible inside a.ts. Also, we do not need to add b.ts in the compilation process, therefore the tsc a.ts command will do the job.

// a.js
var ross = MyLibA.getPerson("Ross Geller", 30);

// b.js
var MyLibA;
(function (MyLibA) {
  function getPerson(name, age) {
    return { name: name, age: age };
  }
  MyLibA.getPerson = getPerson;
})(MyLibA || (MyLibA = {}));

We can’t run this project on Node since these are two separate files and we do not have require() statements in the compiled code that Node can use to load dependent files. We would first need to combine them as a single bundle and run using the command $ node *bundle.js*.

In the browser environment, we need to load b.js first and then a.js since a.js depends on b.js for MyLibA object initialization. So the <script> statement should look like this.

<script src="./b.js" />
<script src="./a.js" />

However, you can use the --outFile flag with the tsc command to generate a bundle (_such as tsc --outFile bundle.js a.ts). The TypeScript compiler figures out the code order in the compiled JavaScript based on the order of reference directives. Therefore, reference directives are necessary for internal modules.

// bundle.js
// (from b.ts)
var MyLibA;
(function (MyLibA) {
  function getPerson(name, age) {
    return { name: name, age: age };
  }

  MyLibA.getPerson = getPerson;
})(MyLibA || (MyLibA = {}));

// (from a.ts)
var ross = MyLibA.getPerson("Ross Geller", 30);

Extending Namespaces

In the interfaces lesson, we saw that when multiple interfaces with the same name are declared in the same module (file), TypeScript coerces them into one declaration by merging their properties together.

You can extend a predefined namespace by referencing the file which contains the original namespace (using the reference directive) and redeclaring the namespace with new values.

// a.ts
/// <reference path="./b.ts"/>
const john: MyLibA.Person = MyLibA.defaultPerson;
const ross: MyLibA.Person = MyLibA.getPerson("Ross Geller", 30);
console.log(john); // {name: 'John Doe', age: 21}
console.log(ross); // {name: 'Ross Geller', age: 30}

// b.ts
/// <reference path="./c.ts" />
namespace MyLibA {
  export const defaultPerson: Person = getPerson("John Doe", 21);
}

// c.ts
namespace MyLibA {
  export interface Person {
    name: string;
    age: number;
  }

  export function getPerson(name: string, age: number): Person {
    return { name, age };
  }
}

In this example, since b.ts references c.ts, it has access to MyLibA namespace and it adds defaultPerson public value to this namespace. If you notice, the MyLibA namespace in the b.ts has access to all the public values (exported only) of the same namespace defined in c.ts.

The a.ts references b.ts and for it, the MyLibA namespace has Person, getPerson and defaultPerson members. When we bundle this project using the tsc --outFile bundle.js a.ts command, we get the following output.

// bundle.js
// (from c.ts)
var MyLibA;
(function (MyLibA) {
  function getPerson(name, age) {
    return { name: name, age: age };
  }
  MyLibA.getPerson = getPerson;
})(MyLibA || (MyLibA = {}));

// (from b.ts)
var MyLibA;
(function (MyLibA) {
  MyLibA.defaultPerson = MyLibA.getPerson("John Doe", 21);
})(MyLibA || (MyLibA = {}));

// (from a.ts)
var john = MyLibA.defaultPerson;
var ross = MyLibA.getPerson("Ross Geller", 30);

Extending the namespace makes much more sense when we see the compiled JavaScript code. As you can see, the middle section belong to the b.ts file which adds defaultPerson property to the MyLibA object.


Now the million-dollar question, where we should use namespaces? My suggestion would be to avoid it whenever you can. We have a standard for modules in JavaScript now. Node.js would also one day fully support it but for now, you can set --module to CommonJS, it is that easy.

Namespaces predate JavaScript modules, therefore it’s not worth investing your money into something that will soon become obsolete. However, namespaces would be a good fit for cross-browser JavaScript applications where the ECMAScript module system is not available in older browsers but we have Webpack and other bundling tools to make ECMAScript module cross-platform and backward compatible.

TypeScript doesn’t allow bundling of ECMAScript or CommonJS modules, which means you can’t have a single bundle file of a TypeScript project that uses these module systems. If having a single bundle file is mission-critical, then you can opt-in for namespaces. Again, Webpack or other bundling tools can help you create a bundle without having to sacrifice anything.

#typescript #namespaces