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 letvar or const variable declaration or classfunction 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.

#typescript #deno #programming #javascript #nodejs

TypeScript — Namespaces
1.50 GEEK