Previously, every page that was loaded had to explicitly reference all script files it needed, whether directly or indirectly. Besides being tedious, this approach was also prone to errors, which led some teams to rely on build processes to automate the task. These processes are capable, among other things, of automatically inserting references to JavaScript files into the individual pages.
EcmaScript 6 offers a better solution with its module system. It stipulates that each file represents a module with its own namespace. This puts an end to the era where declarations could all too easily pollute the global namespace. It also eliminates the need for self-executing anonymous functions. Ultimately, this results in less code and thus greater clarity.
Starting with TypeScript 1.5, this new language feature can be utilised. By compiling to standard EcmaScript 5, the code developed with it can be executed in any browser.
Compiling EcmaScript modules
Several patterns have been described in the past for emulating modules in EcmaScript 5. These patterns define modules as functions so that they receive their own namespace, and they expose the constructs needed in other modules via a data structure. TypeScript 1.5 supports four of these patterns. These include the CommonJS modules known from the NodeJS world and the Asynchronous Module Definition (AMD) supported by Require.js. In addition, it supports a format known from the SystemJS world as well as the young Universal Module Definition (UMD), which combines, among other things, the CommonJS and AMD formats.
For TypeScript to compile modules down to EcmaScript 5 or 3, the desired module format must be specified. When using Visual Studio 2015, this is done through the project settings (more on that here). In the case under consideration, the choice falls on AMD, which is designed for use in browsers.
If Visual Studio is not used, but instead a direct invocation of the TypeScript compiler or a tool such as Visual Studio Code, the development team can store the desired module format in the tsconfig.json file. The TypeScript compiler automatically takes into account the settings contained therein when it finds them in the current working directory. When using Visual Studio Code, this is the root directory of the application. An example of this is provided below. It once again specifies the AMD format, defines that source maps are to be created, and instructs TypeScript to compile to EcmaScript 5.
{
"compilerOptions": {
"module": "amd",
"sourceMap": true,
"target": "ES5"
}
}
Exporting constructs from a module
If a module wants to make classes, functions, or variables available to other modules, it must export these constructs. The next listing provides an example of this. It shows the contents of a file flug.ts, which—as is customary from EcmaScript 6 onwards—forms a module. Within it, there is a class Flug with a few properties. Apart from the type annotations supported by TypeScript, this is pure EcmaScript 6. To ensure that the class Flug is also available outside the module, it has been marked with the export keyword.
An alternative syntax can be found in the comment at the end of the listing. This initially omits the export keyword and thus declares the class as module-internal. A standalone export statement subsequently defines which internal constructs are to be published. Multiple constructs can be listed separated by commas. In this way, all exports can be defined collectively at the end of a file.
// flug.ts
export class Flug {
id: number;
fnr: string;
datum: Date;
abflugort: string;
zielort: string;
plaetze: number;
plaetzeFrei: number;
}
// Alternative
// class Flug { […] }
// export { Flug };
Importing exported constructs
To use exported constructs in other modules, they must be imported there. For this purpose, EcmaScript 6 provides the import keyword. Listing 3, which shows the contents of a file flugCalculator.ts, provides an example. It first uses an import statement to bring in the class Flug exported in Listing 2. The name of this class appears in curly braces; the name of the file that exports Flug appears within the string following the from keyword. File extensions are omitted.
Files in other folders are referenced by specifying paths. For example, 'entities/Flug' points to the file Flug in the entities folder. To ensure that the search for a file occurs relative to the current file and not relative to the root of the web application, a path must be prefixed with a dot: './entities/Flug'.
// flugCalculator.ts
import { Flug } from 'flug';
export class FlugCalculator {
static basePrice = 300;
calcPrice(flug: Flug) {
var p = flug.plaetze / flug.plaetzeFrei;
if (p >= 0.75) return FlugCalculator.basePrice / 2;
return FlugCalculator.basePrice;
}
}
Loading modules
For loading modules, the page being accessed requires a so-called module loader. Examples of these are RequireJS and SystemJS. The examples considered here used the latter, especially since it supports all common module formats. It is advisable to use SystemJS together with the package manager JSPM. JSPM fetches JavaScript libraries such as jQuery or AngularJS in the form of modules. To do this, it accesses various online repositories such as GitHub. To install JSPM, the development team uses the package manager of NodeJS: npm install -g jspm. A call to jspm init in the application root sets up a configuration file and supplies the project with SystemJS.
After SystemJS has been integrated, the page can load the desired module using System.import:
<script src="jspm_packages/system.js"></script>
<script>System.import('app').catch(console.error.bind(console));</script>
The app module loaded here is, for the sake of completeness, found in the next listing. It references the two other modules considered previously and works with the classes they provide.
The System.import function takes the name of the desired module and returns a Promise. For error handling, it is advisable to register an error handler using catch. The example discussed here uses the function console.error, which writes an error to the JavaScript console, for this purpose.
As a rule, it is sufficient to load a single file via the module loader of choice. This file can then reference other modules through import statements. The module loader follows these statements and loads the entire dependency graph thereby described—that is, all directly and indirectly referenced modules.
import { FlugCalculator } from 'flugCalculator';
import { Flug } from 'flug';
var fc = new FlugCalculator();
var f = new Flug();
f.id = 17; f.plaetze = 100; f.plaetzeFrei = 90;
var preis = fc.calcPrice(f);
alert(preis);
