Introduction
Starting with ECMAScript 2015, JavaScript has a 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.
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. Well-known module loaders used in JavaScript are Node.js’s loader for CommonJS modules and the RequireJS loader for AMD modules in Web applications.
In TypeScript, just as in ECMAScript 2015, any file containing a top-level import
or export
is considered a module.
Conversely, a file without any top-level import
or export
declarations is treated as a script whose contents are available in the global scope (and therefore to modules as well).
Export
Exporting a declaration
Any declaration (such as a variable, function, class, type alias, or interface) can be exported by adding the export
keyword.
StringValidator.ts
tsexport interface StringValidator { isAcceptable(s: string): boolean; }
ZipCodeValidator.ts
tsimport { StringValidator } from "./StringValidator"; 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:
tsclass 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
tsexport 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
tsexport * from "./StringValidator"; // exports 'StringValidator' interface export * from "./ZipCodeValidator"; // exports 'ZipCodeValidator' and const 'numberRegexp' class export * from "./ParseIntBasedZipCodeValidator"; // exports the 'ParseIntBasedZipCodeValidator' class // and re-exports 'RegExpBasedZipCodeValidator' as alias // of the 'ZipCodeValidator' class from 'ZipCodeValidator.ts' // module.
Import
Importing is just about as easy as exporting from a module.
Importing an exported declaration is done through using one of the import
forms below:
Import a single export from a module
tsimport { ZipCodeValidator } from "./ZipCodeValidator"; let myValidator = new ZipCodeValidator();
imports can also be renamed
tsimport { 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
tsimport * as validator from "./ZipCodeValidator"; let myValidator = new validator.ZipCodeValidator();
Import a module for side-effects only
Though not recommended practice, some modules set up some global state that can be used by other modules. These modules may not have any exports, or the consumer is not interested in any of their exports. To import these modules, use:
tsimport "./my-module.js";
Importing Types
Prior to TypeScript 3.8, you can import a type using import
.
With TypeScript 3.8, you can import a type using the import
statement, or using import type
.
ts// Re-using the same import import {APIResponseType} from "./api"; // Explicitly use import type import type {APIResponseType} from "./api";
import type
is always guaranteed to be removed from your JavaScript, and tools like Babel can make better assumptions about your code via the isolatedModules
compiler flag.
You can read more in the 3.8 release notes.
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
tsdeclare let $: JQuery; export default $;
App.ts
tsimport $ 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
tsexport default class ZipCodeValidator { static numberRegexp = /^[0-9]+$/; isAcceptable(s: string) { return s.length === 5 && ZipCodeValidator.numberRegexp.test(s); } }
Test.ts
tsimport validator from "./ZipCodeValidator"; let myValidator = new validator();
or
StaticZipCodeValidator.ts
tsconst numberRegexp = /^[0-9]+$/; export default function(s: string) { return s.length === 5 && numberRegexp.test(s); }
Test.ts
tsimport validate from "./StaticZipCodeValidator"; let strings = ["Hello", "98052", "101"]; // Use function validate strings.forEach(s => { console.log(`"${s}" ${validate(s) ? "matches" : "does not match"}`); });
default
exports can also be just values:
OneTwoThree.ts
tsexport default "123";
Log.ts
tsimport num from "./OneTwoThree"; console.log(num); // "123"
Export all as x
With TypeScript 3.8, you can use export * as ns
as a shorthand for re-exporting another module with a name:
tsexport * as utilities from "./utilities";
This takes all of the dependencies from a module and makes it an exported field, you could import it like this:
tsimport { utilities } from "./index";
export =
and import = require()
Both CommonJS and AMD generally have the concept of an exports
object which contains all exports from a module.
They also support replacing the exports
object with a custom single object.
Default exports are meant to act as a replacement for this behavior; however, the two are incompatible.
TypeScript supports export =
to model the traditional CommonJS and AMD workflow.
The export =
syntax specifies a single object that is exported from the module.
This can be a class, interface, namespace, function, or enum.
When exporting a module using export =
, TypeScript-specific import module = require("module")
must be used to import the module.
ZipCodeValidator.ts
tslet numberRegexp = /^[0-9]+$/; class ZipCodeValidator { isAcceptable(s: string) { return s.length === 5 && numberRegexp.test(s); } } export = ZipCodeValidator;
Test.ts
tsimport zip = require("./ZipCodeValidator"); // Some samples to try let strings = ["Hello", "98052", "101"]; // Validators to use let validator = new zip(); // Show whether each string passed each validator strings.forEach(s => { console.log( `"${s}" - ${validator.isAcceptable(s) ? "matches" : "does not match"}` ); });
Code Generation for Modules
Depending on the module target specified during compilation, the compiler will generate appropriate code for Node.js (CommonJS), require.js (AMD), UMD, SystemJS, or ECMAScript 2015 native modules (ES6) module-loading systems.
For more information on what the define
, require
and register
calls in the generated code do, consult the documentation for each module loader.
This simple example shows how the names used during importing and exporting get translated into the module loading code.
SimpleModule.ts
tsimport m = require("mod"); export let t = m.something + 1;
AMD / RequireJS SimpleModule.js
jsdefine(["require", "exports", "./mod"], function(require, exports, mod_1) { exports.t = mod_1.something + 1; });
CommonJS / Node SimpleModule.js
jsvar mod_1 = require("./mod"); exports.t = mod_1.something + 1;
UMD SimpleModule.js
js(function(factory) { if (typeof module === "object" && typeof module.exports === "object") { var v = factory(require, exports); if (v !== undefined) module.exports = v; } else if (typeof define === "function" && define.amd) { define(["require", "exports", "./mod"], factory); } })(function(require, exports) { var mod_1 = require("./mod"); exports.t = mod_1.something + 1; });
System SimpleModule.js
jsSystem.register(["./mod"], function(exports_1) { var mod_1; var t; return { setters: [ function(mod_1_1) { mod_1 = mod_1_1; } ], execute: function() { exports_1("t", (t = mod_1.something + 1)); } }; });
Native ECMAScript 2015 modules SimpleModule.js
jsimport { something } from "./mod"; export var t = something + 1;
Simple Example
Below, we’ve consolidated the Validator implementations used in previous examples to only export a single named export from each module.
To compile, we must specify a module target on the command line. For Node.js, use --module commonjs
;
for require.js, use --module amd
. For example:
tsc --module commonjs Test.ts
When compiled, each module will become a separate .js
file.
As with reference tags, the compiler will follow import
statements to compile dependent files.
Validation.ts
tsexport interface StringValidator { isAcceptable(s: string): boolean; }
LettersOnlyValidator.ts
tsimport { StringValidator } from "./Validation"; const lettersRegexp = /^[A-Za-z]+$/; export class LettersOnlyValidator implements StringValidator { isAcceptable(s: string) { return lettersRegexp.test(s); } }
ZipCodeValidator.ts
tsimport { StringValidator } from "./Validation"; const numberRegexp = /^[0-9]+$/; export class ZipCodeValidator implements StringValidator { isAcceptable(s: string) { return s.length === 5 && numberRegexp.test(s); } }
Test.ts
tsimport { StringValidator } from "./Validation"; import { ZipCodeValidator } from "./ZipCodeValidator"; import { LettersOnlyValidator } from "./LettersOnlyValidator"; // Some samples to try let strings = ["Hello", "98052", "101"]; // Validators to use let validators: { [s: string]: StringValidator } = {}; validators["ZIP code"] = new ZipCodeValidator(); validators["Letters only"] = new LettersOnlyValidator(); // Show whether each string passed each validator strings.forEach(s => { for (let name in validators) { console.log( `"${s}" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${name}` ); } });
Optional Module Loading and Other Advanced Loading Scenarios
In some cases, you may want to only load a module under some conditions. In TypeScript, we can use the pattern shown below to implement this and other advanced loading scenarios to directly invoke the module loaders without losing type safety.
The compiler detects whether each module is used in the emitted JavaScript.
If a module identifier is only ever used as part of a type annotations and never as an expression, then no require
call is emitted for that module.
This elision of unused references is a good performance optimization, and also allows for optional loading of those modules.
The core idea of the pattern is that the import id = require("...")
statement gives us access to the types exposed by the module.
The module loader is invoked (through require
) dynamically, as shown in the if
blocks below.
This leverages the reference-elision optimization so that the module is only loaded when needed.
For this pattern to work, it’s important that the symbol defined via an import
is only used in type positions (i.e. never in a position that would be emitted into the JavaScript).
To maintain type safety, we can use the typeof
keyword.
The typeof
keyword, when used in a type position, produces the type of a value, in this case the type of the module.
Dynamic Module Loading in Node.js
tsdeclare function require(moduleName: string): any; import { ZipCodeValidator as Zip } from "./ZipCodeValidator"; if (needZipValidation) { let ZipCodeValidator: typeof Zip = require("./ZipCodeValidator"); let validator = new ZipCodeValidator(); if (validator.isAcceptable("...")) { /* ... */ } }
Sample: Dynamic Module Loading in require.js
tsdeclare function require( moduleNames: string[], onLoad: (...args: any[]) => void ): void; import * as Zip from "./ZipCodeValidator"; if (needZipValidation) { require(["./ZipCodeValidator"], (ZipCodeValidator: typeof Zip) => { let validator = new ZipCodeValidator.ZipCodeValidator(); if (validator.isAcceptable("...")) { /* ... */ } }); }
Sample: Dynamic Module Loading in System.js
tsdeclare const System: any; import { ZipCodeValidator as Zip } from "./ZipCodeValidator"; if (needZipValidation) { System.import("./ZipCodeValidator").then((ZipCodeValidator: typeof Zip) => { var x = new ZipCodeValidator(); if (x.isAcceptable("...")) { /* ... */ } }); }
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 larger .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)
tsdeclare 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"
.
ts/// <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
tsdeclare module "hot-new-module";
All imports from a shorthand module will have the any
type.
tsimport x, { y } from "hot-new-module"; x(y);
Wildcard module declarations
Some module loaders such as SystemJS and AMD allow non-JavaScript content to be imported. These typically use a prefix or suffix to indicate the special loading semantics. Wildcard module declarations can be used to cover these cases.
tsdeclare module "*!text" { const content: string; export default content; } // Some do it the other way around. declare module "json!*" { const value: any; export default value; }
Now you can import things that match "*!text"
or "json!*"
.
tsimport fileContent from "./xyz.txt!text"; import data from "json!http://example.com/data.json"; console.log(data, fileContent);
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
tsexport function isPrime(x: number): boolean; export as namespace mathLib;
The library can then be used as an import within modules:
tsimport { 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.)
tsmathLib.isPrime(2);
Guidance for structuring modules
Export as close to top-level as possible
Consumers of your module should have as little friction as possible when using things that you export. Adding too many levels of nesting tends to be cumbersome, so think carefully about how you want to structure things.
Exporting a namespace from your module is an example of adding too many layers of nesting. While namespaces sometime have their uses, they add an extra level of indirection when using modules. This can quickly become a pain point for users, and is usually unnecessary.
Static methods on an exported class have a similar problem - the class itself adds a layer of nesting. Unless it increases expressivity or intent in a clearly useful way, consider simply exporting a helper function.
If you’re only exporting a single class
or function
, use export default
Just as “exporting near the top-level” reduces friction on your module’s consumers, so does introducing a default export. If a module’s primary purpose is to house one specific export, then you should consider exporting it as a default export. This makes both importing and actually using the import a little easier. For example:
MyClass.ts
tsexport default class SomeType { constructor() { ... } }
MyFunc.ts
tsexport default function getThing() { return "thing"; }
Consumer.ts
tsimport t from "./MyClass"; import f from "./MyFunc"; let x = new t(); console.log(f());
This is optimal for consumers. They can name your type whatever they want (t
in this case) and don’t have to do any excessive dotting to find your objects.
If you’re exporting multiple objects, put them all at top-level
MyThings.ts
tsexport class SomeType { /* ... */ } export function someFunc() { /* ... */ }
Conversely when importing:
Explicitly list imported names
Consumer.ts
tsimport { SomeType, someFunc } from "./MyThings"; let x = new SomeType(); let y = someFunc();
Use the namespace import pattern if you’re importing a large number of things
MyLargeModule.ts
tsexport class Dog { ... } export class Cat { ... } export class Tree { ... } export class Flower { ... }
Consumer.ts
tsimport * as myLargeModule from "./MyLargeModule.ts"; let x = new myLargeModule.Dog();
Re-export to extend
Often you will need to extend functionality on a module. A common JS pattern is to augment the original object with extensions, similar to how JQuery extensions work. As we’ve mentioned before, modules do not merge like global namespace objects would. The recommended solution is to not mutate the original object, but rather export a new entity that provides the new functionality.
Consider a simple calculator implementation defined in module Calculator.ts
.
The module also exports a helper function to test the calculator functionality by passing a list of input strings and writing the result at the end.
Calculator.ts
tsexport class Calculator { private current = 0; private memory = 0; private operator: string; protected processDigit(digit: string, currentValue: number) { if (digit >= "0" && digit <= "9") { return currentValue * 10 + (digit.charCodeAt(0) - "0".charCodeAt(0)); } } protected processOperator(operator: string) { if (["+", "-", "*", "/"].indexOf(operator) >= 0) { return operator; } } protected evaluateOperator( operator: string, left: number, right: number ): number { switch (this.operator) { case "+": return left + right; case "-": return left - right; case "*": return left * right; case "/": return left / right; } } private evaluate() { if (this.operator) { this.memory = this.evaluateOperator( this.operator, this.memory, this.current ); } else { this.memory = this.current; } this.current = 0; } public handleChar(char: string) { if (char === "=") { this.evaluate(); return; } else { let value = this.processDigit(char, this.current); if (value !== undefined) { this.current = value; return; } else { let value = this.processOperator(char); if (value !== undefined) { this.evaluate(); this.operator = value; return; } } } throw new Error(`Unsupported input: '${char}'`); } public getResult() { return this.memory; } } export function test(c: Calculator, input: string) { for (let i = 0; i < input.length; i++) { c.handleChar(input[i]); } console.log(`result of '${input}' is '${c.getResult()}'`); }
Here is a simple test for the calculator using the exposed test
function.
TestCalculator.ts
tsimport { Calculator, test } from "./Calculator"; let c = new Calculator(); test(c, "1+2*33/11="); // prints 9
Now to extend this to add support for input with numbers in bases other than 10, let’s create ProgrammerCalculator.ts
ProgrammerCalculator.ts
tsimport { Calculator } from "./Calculator"; class ProgrammerCalculator extends Calculator { static digits = [ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F" ]; constructor(public base: number) { super(); const maxBase = ProgrammerCalculator.digits.length; if (base <= 0 || base > maxBase) { throw new Error(`base has to be within 0 to ${maxBase} inclusive.`); } } protected processDigit(digit: string, currentValue: number) { if (ProgrammerCalculator.digits.indexOf(digit) >= 0) { return ( currentValue * this.base + ProgrammerCalculator.digits.indexOf(digit) ); } } } // Export the new extended calculator as Calculator export { ProgrammerCalculator as Calculator }; // Also, export the helper function export { test } from "./Calculator";
The new module ProgrammerCalculator
exports an API shape similar to that of the original Calculator
module, but does not augment any objects in the original module.
Here is a test for our ProgrammerCalculator class:
TestProgrammerCalculator.ts
tsimport { Calculator, test } from "./ProgrammerCalculator"; let c = new Calculator(2); test(c, "001+010="); // prints 3
Do not use namespaces in modules
When first moving to a module-based organization, a common tendency is to wrap exports in an additional layer of namespaces. Modules have their own scope, and only exported declarations are visible from outside the module. With this in mind, namespace provide very little, if any, value when working with modules.
On the organization front, namespaces are handy for grouping together logically-related objects and types in the global scope. For example, in C#, you’re going to find all the collection types in System.Collections. By organizing our types into hierarchical namespaces, we provide a good “discovery” experience for users of those types. Modules, on the other hand, are already present in a file system, necessarily. We have to resolve them by path and filename, so there’s a logical organization scheme for us to use. We can have a /collections/generic/ folder with a list module in it.
Namespaces are important to avoid naming collisions in the global scope.
For example, you might have My.Application.Customer.AddForm
and My.Application.Order.AddForm
— two types with the same name, but a different namespace.
This, however, is not an issue with modules.
Within a module, there’s no plausible reason to have two objects with the same name.
From the consumption side, the consumer of any given module gets to pick the name that they will use to refer to the module, so accidental naming conflicts are impossible.
For more discussion about modules and namespaces see Namespaces and Modules.
Red Flags
All of the following are red flags for module structuring. Double-check that you’re not trying to namespace your external modules if any of these apply to your files:
- A file whose only top-level declaration is
export namespace Foo { ... }
(removeFoo
and move everything ‘up’ a level) - Multiple files that have the same
export namespace Foo {
at top-level (don’t think that these are going to combine into oneFoo
!)