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.