Return to site

Modules

Modules are a way to create a local scope in the file. So, all variables, classes, functions, etc. that are declared in a module are not accessible outside the module. A module can be created using the keyword export and a module can be used in another module using the keyword import.

· angular

As mentioned earlier, Modules are used for code organization as well as code sharing. The older name for modules is "external modules", starting TypeScript 1.5, they are just called "modules".

 

Starting with the ECMAScript 2015, JavaScript has the concept of modules. TypeScript shares this concept.

 

Modules are executed within their own scope, not in the global scope; this means that variables, functions, classes, etc. declared in a module are not visible outside the module unless they are explicitly exported using one of the export forms. Conversely, to consume a variable, function, class, interface, etc. exported from a different module, it has to be imported using one of the import forms.

 

Modules are declarative; the relationships between modules are specified in terms of imports and exports at the file level.

 

In TypeScript any file containing a top-level import or export is considered a module. This is similar to ECMAScript 2015. This is also different than namespaces that use the namespace keyword to declare a namespace.

 

Module Loaders

Modules import one another using a module loader. At runtime the module loader is responsible for locating and executing all dependencies of a module before executing it, this is called Module Resolution. Well-known modules loaders used in JavaScript are the CommonJS module loader for Node.js and require.js for Web applications.

 

Benefits of using module loaders

·       Asynchronous module loading.

·       Lazy (on-demand) module loading.

·       Great community support for example, one of the standard module wrappers (AMD) has a lot of community modules implement it, so your modules can depend on them.

An example of a module loader

RequireJS is a JavaScript file and module loader. It is optimized for in-browser use, but it can be used in other JavaScript environments, like Rhino and Node. Using a modular script loader like RequireJS will improve the speed and quality of your code. RequireJS only loads the JS script that the page needs.

Check this article for more about the benefits of module loaders:

 

https://www.quora.com/Why-do-people-use-requireJS-What-are-the-benefits

 

More on module loading is covered in the demo video for this lesson.

 

Export

Exporting a declaration

Any declaration (such as a variable, function, class, type alias, or interface) can be exported by adding the export keyword.

 

Validation.ts

export interface StringValidator {

    isAcceptable(s: string): boolean;

}

ZipCodeValidator.ts

export const numberRegexp = /^[0-9]+$/;

 

export class ZipCodeValidator implements StringValidator {

    isAcceptable(s: string) {

        return s.length === 5 && numberRegexp.test(s);

    }

}

 

Export statements

Export statements are handy when exports need to be renamed for consumers, so the above example can be written as:

 

class ZipCodeValidator implements StringValidator {

    isAcceptable(s: string) {

        return s.length === 5 && numberRegexp.test(s);

    }

}

export { ZipCodeValidator };

export { ZipCodeValidator as mainValidator };

 

Re-exports

Often modules extend other modules, and partially expose some of their features. A re-export does not import it locally, or introduce a local variable.

 

ParseIntBasedZipCodeValidator.ts

export class ParseIntBasedZipCodeValidator {

    isAcceptable(s: string) {

        return s.length === 5 && parseInt(s).toString() === s;

    }

}

 

// Export original validator but rename it

export {ZipCodeValidator as RegExpBasedZipCodeValidator} from "./ZipCodeValidator";

 

Optionally, a module can wrap one or more modules and combine all their exports using export * from "module" syntax.

 

AllValidators.ts

export * from "./StringValidator"; // exports interface 'StringValidator'

export * from "./LettersOnlyValidator"; // exports class 'LettersOnlyValidator'

export * from "./ZipCodeValidator";  // exports class 'ZipCodeValidator'

 

Import

Importing an exported declaration is done through using one of the import forms below:

 

Import a single export from a module

 

import { ZipCodeValidator } from "./ZipCodeValidator";

 

let myValidator = new ZipCodeValidator();

imports can also be renamed

 

import { ZipCodeValidator as ZCV } from "./ZipCodeValidator";

let myValidator = new ZCV();

 

Import the entire module into a single variable, and use it to access the module exports

 

import * as validator from "./ZipCodeValidator";

let myValidator = new validator.ZipCodeValidator();

 

Default exports

Each module can optionally export a default export. Default exports are marked with the keyword default; and there can only be one default export per module. default exports are imported using a different import form.

 

default exports are really handy. For instance, a library like JQuery might have a default export of jQuery or $, which we'd probably also import under the name $ or jQuery.

 

JQuery.d.ts

declare let $: JQuery;

export default $;

App.ts

import $ from "JQuery";

 

$("button.continue").html( "Next Step..." );

 

Classes and function declarations can be authored directly as default exports. Default export class and function declaration names are optional.

 

 

ZipCodeValidator.ts

export default class ZipCodeValidator {

    static numberRegexp = /^[0-9]+$/;

    isAcceptable(s: string) {

        return s.length === 5 && ZipCodeValidator.numberRegexp.test(s);

    }

}

Test.ts

import validator from "./ZipCodeValidator";

 

let myValidator = new validator();

 

or

 

StaticZipCodeValidator.ts

const numberRegexp = /^[0-9]+$/;

 

export default function (s: string) {

    return s.length === 5 && numberRegexp.test(s);

}

Test.ts

import validate from "./StaticZipCodeValidator";

 

let strings = ["Hello", "98052", "101"];

 

// Use function validate

strings.forEach(s => {

  console.log(`"${s}" ${validate(s) ? " matches" : " does not match"}`);

});

 

Declaration Files

Declarations are used to describe code that exists elsewhere (e.g. written in JavaScript, CoffeeScript, or nodejs) using the declare keyword. The goal of this is to be able to use this code in TypeScript applications without having to rewrite the code in TypeScript.

 

For example:

 

 class="language-TypeScript">mynumber = 200; // Error: mynumber is not defined

 

while

 

declare var mynumber: any;

mynumber = 200; // ok

 

You can save these declarations in a .ts file or in a .d.ts file, we call these files with .d.ts extension declaration files.

It is a good practice to keep your declarations in separate .d.ts file.

 

If a file has the extension .d.ts then each root level definition must have the declare keyword prefixed to it. This tells the developer that there will be no code emitted by TypeScript. The developer needs to ensure that the declared item will exist at runtime.

 

For example, lets assume we have the following JavaScript (message.js):

 

function showMessage(message) {

    alert(message);

}

 

If you want to call the showMessage function in your TypeScript code, TypeScript will not recognize that this function exists. It does not know its name or its parameters. This can be fixed by describing the showMessage function in a definition file as (message.d.ts):

 

declare function showMessage(message: string);

 

Now you can use the function showMessage in TypeScript without compile errors. youalso will get compile errors if you try to use it incorrectly (for example, passing 2 arguments instead of 1).

 

.d.ts files and .ts files

Anything allowed in a .d.ts file may also appear in a .ts file, but not the reverse. Therefore, .d.ts allows a subset of TypeScript's features. A .d.ts file is only allowed to contain TypeScript code that doesn't generate any JavaScript code in the output. If you attempt to use any feature of TypeScript that would generate JavaScript, you'll get an error. Interfaces are allowed, because they disappear completely after compilation. Const enums (added in TS1.4) are also allowed, unlike ordinary enums which generate an object in the output JavaScript. Top level classes, variables, modules and functions must be prefixed with declare. It is a common practice to see a top-level declare module and then all definitions inside it are therefore also declaration. For example:

 

declare module Something {

    var x;

}

 

Ambient Modules and JavaScript libraries

Working with Other JavaScript Libraries

To describe the shape of libraries not written in TypeScript, we need to declare the API that the library exposes.

 

We call declarations that don't define an implementation "ambient". Typically, these are defined in .d.ts files. If you're familiar with C/C++, you can think of these as .h files. Let's look at a few examples.

 

Ambient Modules

In Node.js, most tasks are accomplished by loading one or more modules. We could define each module in its own .d.ts file with top-level export declarations, but it's more convenient to write them as one large .d.ts file. To do so, we use a construct similar to ambient namespaces, but we use the module keyword and the quoted name of the module which will be available to a later import. For example:

 

node.d.ts (simplified excerpt)

declare module "url" {

    export interface Url {

        protocol?: string;

        hostname?: string;

        pathname?: string;

    }

 

    export function parse(urlStr: string, parseQueryString?, slashesDenoteHost?): Url;

}

 

declare module "path" {

    export function normalize(p: string): string;

    export function join(...paths: any[]): string;

    export var sep: string;

}

 

Now we can /// <reference> node.d.ts and then load the modules using import url = require("url"); or import * as URL from "url".

 

/// <reference path="node.d.ts"/>

import * as URL from "url";

let myUrl = URL.parse("http://www.typescriptlang.org");

 

Shorthand ambient modules

If you don't want to take the time to write out declarations before using a new module, you can use a shorthand declaration to get started quickly.

 

declarations.d.ts

declare module "hot-new-module";

 

All imports from a shorthand module will have the any type.

 

import x, {y} from "hot-new-module";

x(y);

 

UMD modules

Some libraries are designed to be used in many module loaders, or with no module loading (global variables). These are known as UMD modules. These libraries can be accessed through either an import or a global variable. For example:

 

math-lib.d.ts

export const isPrime(x: number): boolean;

export as namespace mathLib;

 

The library can then be used as an import within modules:

 

import { isPrime } from "math-lib";

isPrime(2);

mathLib.isPrime(2); // ERROR: can't use the global definition from inside a module

 

It can also be used as a global variable, but only inside of a script. (A script is a file with no imports or exports.)

 

mathLib.isPrime(2);

 

Module Resolution

Introduction:

Module resolution is the process the compiler uses to figure out what an import refers to. Consider an import statement like import { a } from "moduleA"; in order to check any use of a, the compiler needs to know exactly what it represents, and will need to check its definition moduleA.

 

At this point, the compiler will ask "what's the shape of moduleA?" While this sounds straightforward, moduleA could be defined in one of your own .ts/.tsx files, or in a .d.ts that your code depends on.

 

First, the compiler will try to locate a file that represents the imported module. To do so the compiler follows one of two different strategies: Classic or Node. These strategies tell the compiler where to look for moduleA.

 

If that didn't work and if the module name is non-relative (and in the case of "moduleA", it is), then the compiler will attempt to locate an ambient module declaration.

 

Finally, if the compiler could not resolve the module, it will log an error. In this case, the error would be something like error TS2307: Cannot find module 'moduleA'.

 

Relative vs. Non-relative module imports

Module imports are resolved differently based on whether the module reference is relative or non-relative.

 

A relative import is one that starts with /, ./ or ../. Some examples include:

 

·       import Entry from "./components/Entry";

·       import { DefaultHeaders } from "../constants/http";

·       import "/mod";

 

Any other import is considered non-relative. Some examples include:

 

·       import * as $ from "jquery";

·       import { Component } from "@angular/core";

 

A relative import is resolved relative to the importing file and cannot resolve to an ambient module declaration. You should use relative imports for your own modules that are guaranteed to maintain their relative location at runtime.

 

A non-relative import can be resolved relative to baseUrl, or through path mapping. They can also resolve to ambient module declarations.

 

Use non-relative paths when importing any of your external dependencies.

 

Module Resolution Strategies

There are two possible module resolution strategies: Node and Classic. You can use the --moduleResolution flag to specify the module resolution strategy. If not specified, the default is Classic for --module AMD | System | ES2015 or Node otherwise.

 

Classic

This used to be TypeScript's default resolution strategy. Nowadays, this strategy is mainly present for backward compatibility.

 

A relative import will be resolved relative to the importing file. So import { b } from "./moduleB" in source file /root/src/folder/A.ts would result in the following lookups:

 

1.       /root/src/folder/moduleB.ts

2.       /root/src/folder/moduleB.d.ts

3.        

For non-relative module imports, however, the compiler walks up the directory tree starting with the directory containing the importing file, trying to locate a matching definition file.

 

For example:

 

A non-relative import to moduleB such as import { b } from "moduleB", in a source file /root/src/folder/A.ts, would result in attempting the following locations for locating "moduleB":

 

1.       /root/src/folder/moduleB.ts

2.       /root/src/folder/moduleB.d.ts

3.       /root/src/moduleB.ts

4.       /root/src/moduleB.d.ts

5.       /root/moduleB.ts

6.       /root/moduleB.d.ts

7.       /moduleB.ts

8.       /moduleB.d.ts

 

Node

This resolution strategy attempts to mimic the Node.js module resolution mechanism at runtime.

 

How Node.js resolves modules

To understand what steps the TS compiler will follow, it is important to shed some light on Node.js modules. Traditionally, imports in Node.js are performed by calling a function named require. The behavior Node.js takes will differ depending on if require is given a relative path or a non-relative path.

 

Relative paths are fairly straightforward. As an example, let's consider a file located at /root/src/moduleA.js, which contains the import var x = require("./moduleB"); Node.js resolves that import in the following order:

 

1.       As the file named /root/src/moduleB.js, if it exists.

 

2.       As the folder /root/src/moduleB if it contains a file named package.json that specifies a "main" module. In our example, if Node.js found the file /root/src/moduleB/package.json containing { "main": "lib/mainModule.js" }, then Node.js will refer to /root/src/moduleB/lib/mainModule.js.

 

3.       As the folder /root/src/moduleB if it contains a file named index.js. That file is implicitly considered that folder's "main" module.

 

You can read more about this in Node.js documentation on file modules and folder modules.

 

However, resolution for a non-relative module name is performed differently. Node will look for your modules in special folders named node_modules. A node_modules folder can be on the same level as the current file, or higher up in the directory chain. Node will walk up the directory chain, looking through each node_modules until it finds the module you tried to load.

 

Following up our example above, consider if /root/src/moduleA.js instead used a non-relative path and had the import var x = require("moduleB");. Node would then try to resolve moduleB to each of the locations until one worked.

 

1.       /root/src/node_modules/moduleB.js

2.       /root/src/node_modules/moduleB/package.json (if it specifies a "main" property)

3.       /root/src/node_modules/moduleB/index.js

 

4.       /root/node_modules/moduleB.js

5.       /root/node_modules/moduleB/package.json (if it specifies a "main" property)

6.       /root/node_modules/moduleB/index.js

 

7.       /node_modules/moduleB.js

8.       /node_modules/moduleB/package.json (if it specifies a "main" property)

9.       /node_modules/moduleB/index.js

 

Notice that Node.js jumped up a directory in steps (4) and (7).

 

How TypeScript resolves modules

TypeScript will mimic the Node.js run-time resolution strategy in order to locate definition files for modules at compile-time. To accomplish this, TypeScript overlays the TypeScript source file extensions (.ts, .tsx, and .d.ts) over the Node's resolution logic. TypeScript will also use a field in package.json named "typings" to mirror the purpose of "main" - the compiler will use it to find the "main" definition file to consult.

 

For example, an import statement like import { b } from "./moduleB" in /root/src/moduleA.ts would result in attempting the following locations for locating "./moduleB":

 

1.       /root/src/moduleB.ts

2.       /root/src/moduleB.tsx

3.       /root/src/moduleB.d.ts

4.       /root/src/moduleB/package.json (if it specifies a "typings" property)

5.       /root/src/moduleB/index.ts

6.       /root/src/moduleB/index.tsx

7.       /root/src/moduleB/index.d.ts

 

Recall that Node.js looked for a file named moduleB.js, then an applicable package.json, and then for an index.js.

 

Similarly a non-relative import will follow the Node.js resolution logic, first looking up a file, then looking up an applicable folder. So import { b } from "moduleB" in source file /root/src/moduleA.ts would result in the following lookups:

 

1.       /root/src/node_modules/moduleB.ts

2.       /root/src/node_modules/moduleB.tsx

3.       /root/src/node_modules/moduleB.d.ts

4.       /root/src/node_modules/moduleB/package.json (if it specifies a "typings" property)

5.       /root/src/node_modules/moduleB/index.ts

6.       /root/src/node_modules/moduleB/index.tsx

7.       /root/src/node_modules/moduleB/index.d.ts

 

8.       /root/node_modules/moduleB.ts

9.       /root/node_modules/moduleB.tsx

10.   /root/node_modules/moduleB.d.ts

11.   /root/node_modules/moduleB/package.json (if it specifies a "typings" property)

12.   /root/node_modules/moduleB/index.ts

13.   /root/node_modules/moduleB/index.tsx

14.   /root/node_modules/moduleB/index.d.ts

 

15.   /node_modules/moduleB.ts

16.   /node_modules/moduleB.tsx

17.   /node_modules/moduleB.d.ts

18.   /node_modules/moduleB/package.json (if it specifies a "typings" property)

19.   /node_modules/moduleB/index.ts

20.   /node_modules/moduleB/index.tsx

21.   /node_modules/moduleB/index.d.ts

 

Although it may seem there is too many steps, TypeScript is still only jumping up directories twice at steps (8) and (15). This is really no more complex than what Node.js itself is doing.