[TYPESCRIPT] Employing “Namespaces” in TypeScript to encapsulate your data

https://medium.com/jspoint/typescript-namespaces-f43cd002c08c

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.

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 can not 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 the above 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 the above modification, we have wrapped the application logic code inside the namespace MyLibA. The namespace { ... } block is like a prison for the code inside. It can’t escape outside which means it can’t pollute the global scope. Therefore, nobody from the outside can access the values inside.

$ 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 the above example, we have added export keyword before the getVersion function to make it publically 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.

The syntax to export values from a namespace is as simple as putting export before a declaration whether it is a let, var or const variable declaration or class, function or even enum declaration as illustrated below.

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 the above 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 below.

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 the above 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 the above example, the namespace MyLibA exports two namespaces viz. Types and Functions. Namespaces are lexically scoped, therefore the getPerson function 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

In the previous example, we can see that the nested namespace can get messy and difficult to use pretty quickly. The MyLibA.Functions.getPerson syntax is quite a mouthful. To shorten this, we can reference it using a variable.

// 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 the above example, we have saved MyLibA.Functions into a constant API which is short, cute, and easy to use. However, the same doesn’t work for Person since Person is declared as a variable but MyLibA.Types.Person is a type. You could use type Person = MyLibA.Types.Person to make this work.

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 the above 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 the above 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 the example above, 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 the below 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 to create a dependency. This is kinda similar to importing b.ts using import statement but without having to mention the import 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 the above 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 --outFile flag with the tsc command to generate a bundle (such as tsc --outFile bundle.js a.ts). The typeScript compiler will automatically figure out the order of code precedence in the compiled JavaScript based on the order of reference directives. Therefore reference directives are absolutely 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 the above 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.

Last updated

Navigation

Lionel

@Copyright 2023