Description
This issue describes TypeScript's support for ECMAScript 6 modules as implemented in #1983, #2197, and #2460.
TypeScript 1.5 supports ECMAScript 6 (ES6) modules. ES6 modules are effectively TypeScript external modules with a new syntax: ES6 modules are separately loaded source files that possibly import other modules and provide a number of externally accessible exports. ES6 modules feature several new export and import declarations. It is recommended that TypeScript libraries and applications be updated to use the new syntax, but this is not a requirement. The new ES6 module syntax coexists with TypeScript's original internal and external module constructs and the constructs can be mixed and matched at will.
In TypeScript 1.5, a source file is considered an external module if it contains at least one of the following:
- A top-level declaration that specifies an
export
modifier. - An new ES6 export or import declaration of any form.
- An original TypeScript export-equals assignment of the form
export = Point
. - An original TypeScript import-equals statement of the form
import Math = require("math")
.
An external module has a set of exports that are specified using various forms of export declarations. Those exports can be imported into local name bindings in other modules using various forms of import declarations.
An external module may designate a default export, which is an export with the reserved name default
. A number of short-hand export and import declaration constructs exist to facilitate easy export and import of the default entity.
For backwards compatibility with CommonJS and AMD style modules, TypeScript also supports export-equals declarations of the form export = Point
. Unlike default export declarations, which are just shorthand for an export named default
, export-equals declarations designate an entity to be exported in place of the actual module.
As ES6 modules gain adoption, TypeScript's original export-equals and import-equals declarations are expected to become legacy.
Export Declarations
When a declaration specifies an export
modifier, each declared name is exported from the containing module exactly as is the case with original TypeScript external modules. For example:
export interface Stream { ... }
export function write(stream: Stream, data: string) { ... }
Module members can also be exported using separate export declarations, and such declarations can specify different names for exports using as
clauses. For example:
interface Stream { ... }
function writeToStream(stream: Stream, data: string) { ... }
export { Stream, writeToStream as write }; // writeToStream exported as write
An export declaration exports all meanings of a name. For example:
interface Stream { ... }
function Stream(url: string): Stream { ... }
export { Stream }; // Exports both interface and function
Re-exporting
An export declaration that specifies a from
clause is a re-export. A re-export copies the exports of a given module to the current module without introducing local names.
export { read, write, standardOutput as stdout } from "./inout";
An export *
declaration can be used to re-export all exports of another module. This is useful for creating modules that aggregate the exports of several other modules.
export function transform(s: string): string { ... }
export * from "./mod1";
export * from "./mod2";
An export *
doesn't re-export default exports or exports with names that are already exported from the current module. For example, the transform
export in the module above hides any transform
export in the re-exported modules.
Default Export
An export default declaration specifies an expression that becomes the default export of a module:
export default {
name: "hello",
count: 42
};
An export default declaration is just a short-hand way of exporting an entity with the name default
. For example, the module above could instead be written:
const x = {
name: "hello",
count: 42
};
export { x as default };
When an export default specifies a single identifier, all meanings of that identifier are exported:
interface Stream { ... }
function Stream(url: string): Stream { ... }
export default Stream; // Exports a type and a value
An export default declaration can directly declare and export a function or class. The function or class can optionally be named so it can be referenced in the implementing module, but the exported name is always default
.
The following exports an unnamed function with the exported name default
:
export default function (x: number) {
return x * x;
}
The following exports a class with the local name Greeter
and the exported name default
:
export default class Greeter {
sayHello() {
console.log("Greetings!");
}
}
Import Declarations
The exports of a module are imported using import declarations. Import declarations can optionally use as
clauses to specify different local names for the imports. For example:
import { read, write, standardOutput as stdout } from "./inout";
var s = read(stdout);
write(stdout, s);
As an alternative to individual imports, a namespace import can be used to import an entire module:
import * as io from "./inout";
var s = io.read(io.standardOutput);
io.write(io.standardOutput, s);
Default Import
The default export of a module is particularly easy to import:
import Greeter from "./greeter";
var g = new Greeter();
g.sayHello();
The above is exactly equivalent to importing the export named default
:
import { default as Greeter } from "./greeter";
var g = new Greeter();
g.sayHello();
It is possible to import both the default export and named exports in a single import declaration:
import defaultExport, { namedExport1, namedExport2, namedExport3 } from "./myModule";
Bare Import
A "bare import" can be used to import a module only for its side-effects. Such an import creates no local name bindings.
import "./polyfills";
CommonJS and AMD Code Generation
TypeScript supports down-level compilation of external modules using the new ES6 syntax.
- When compiling with
-t ES3
or-t ES5
a module format must be chosen using-m CommonJS
or-m AMD
. - When compiling with
-t ES6
the module format is implicitly assumed to be ECMAScript 6 and the compiler simply emits the original code with type annotations removed.
When compiling down-level for CommonJS or AMD, named exports are emitted as properties on the loader supplied exports
instance. This includes default exports which are emitted as assignments to exports.default
.
Below are some examples of external modules and the code emitted for CommonJS and AMD.
A module with named exports:
// TypeScript code
function foo() { }
function bar() { }
export { foo, bar as baz };
// Code emitted for CommonJS
function foo() { }
exports.foo = foo;
function bar() { }
exports.baz = bar;
// Code emitted for AMD
define(["require", "exports"], function (require, exports) {
function foo() { }
exports.foo = foo;
function bar() { }
exports.baz = bar;
});
A module with a default export:
// TypeScript code
export default function foo() { }
// Code emitted for CommonJS
function foo() { }
exports.default = foo;
// Code emitted for AMD
define(["require", "exports"], function (require, exports) {
function foo() { }
exports.default = foo;
});
A module with re-exports:
// TypeScript code
export { read, write } from "./inout";
export * from "./utils";
// Code emitted for CommonJS
function __export(m) {
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
}
var inout_1 = require("./inout");
exports.read = inout_1.read;
exports.write = inout_1.write;
__export(require("./utils"));
// Code emitted for AMD
define(["require", "exports", "./inout", "./utils"], function (require, exports, inout_1, utils_1) {
function __export(m) {
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
}
exports.read = inout_1.read;
exports.write = inout_1.write;
__export(utils_1);
});
Importing a module:
// TypeScript code
import { read, write, standardOutput as stdout } from "./inout";
var s = read(stdout);
write(stdout, s);
// Code emitted for CommonJS
var inout_1 = require("./inout");
var s = inout_1.read(inout_1.standardOutput);
inout_1.write(inout_1.standardOutput, s);
// Code emitted for AMD
define(["require", "exports", "./inout"], function (require, exports, inout_1) {
var s = inout_1.read(inout_1.standardOutput);
inout_1.write(inout_1.standardOutput, s);
});
Note that destructuring import declarations are rewritten to property accesses on the imported module object. This ensures that exported members can circularly reference each other. For example:
// ------ ping.ts ------
import { pong } from "./pong";
export function ping(count: number) {
if (count > 0) {
console.log("ping");
pong(count - 1);
}
}
// ------ pong.ts ------
import { ping } from "./ping";
export function pong(count: number) {
if (count > 0) {
console.log("pong");
ping(count - 1);
}
}
// ------ main.ts ------
import { ping } from "./ping";
ping(10);
This generates the following code when compiled for CommonJS:
// ------ ping.js ------
var pong_1 = require("./pong");
function ping(count) {
if (count > 0) {
console.log("ping");
pong_1.pong(count - 1);
}
}
exports.ping = ping;
// ------ pong.js ------
var ping_1 = require("./ping");
function pong(count) {
if (count > 0) {
console.log("pong");
ping_1.ping(count - 1);
}
}
exports.pong = pong;
// ------ main.js ------
var ping_1 = require("./ping");
ping_1.ping(10);
Interoperabitility
An existing external module that doesn't use export =
is already ES6 compliant and can be imported using the new ES6 constructs with no additional work.
An external module that uses export =
to export another module or a "module like" entity can also be imported using the new ES6 constructs. In particular, the convenient destructuring imports can be used with such modules. The pattern of using export =
to export another module is common in .d.ts files that provide a CommonJS/AMD view of an internal module (e.g. angular.d.ts).
A module that uses export =
to export a non-module entity in place of the module itself must be imported using the existing import x = require("foo")
syntax as is the case today.