Repository navigation
ES6 Modules #2242
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
exportmodifier. - 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 writeAn export declaration exports all meanings of a name. For example:
interface Stream { ... }
function Stream(url: string): Stream { ... }
export { Stream }; // Exports both interface and functionRe-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 valueAn 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 ES3or-t ES5a module format must be chosen using-m CommonJSor-m AMD. - When compiling with
-t ES6the 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.
Activity
Alxandr commentedon Mar 7, 2015
Babel handles mangling default exports with named exports just fine in both AMD and CommonJS. This (amongst other thigs) allows for some nice ways to create default instances of classes, like
which results in the following CommonJS code:
When importing this as
it generates the following:
The trick here is the
_interopRequirecall on default imports, that allows it to work with both es6 modules compiled with the same transpiler, as well as regular AMD/CommonJS modules that use the default export paradigm.Alxandr commentedon Mar 8, 2015
Oh, and as a side-note, given that typescript typically has metadata about everything, the
_interopRequirecould be skipped as long as the source is in typescript, or there is a.d.tsfile, as it would be known at compile-time the shape of the module in question.NoelAbrahams commentedon Mar 8, 2015
Anders Hejlsberg (@ahejlsberg),
👍
I suggest the addendum: "TypeScript's original internal and external module constructs are deprecated and may not be supported in future versions".
(With the aim of discouraging multiple ways of doing the same thing.)
Also I couldn't find anything that says what happens when a module is imported and used in a type-only position. I would expect the down-level emit to omit the
requireas it does now.ahejlsberg commentedon Mar 9, 2015
Aleksander Heintz (@Alxandr) It's a nifty scheme, but one issue is that if
export defaultalways creates anexports.defaultproperty we would have to keep the oldexport =syntax around for creating modules that want to assign tomodule.exportsand remain consumable by down-level clients that aren't aware of the trick. We would much prefer to retire the old syntax and have everyone move to ES6 syntax.Alxandr commentedon Mar 9, 2015
Anders Hejlsberg (@ahejlsberg) babel actually deals with this by special casing files that only export a default to use
module.exports. Given that this is a new syntax etc, it would not break compatibility, while still allowing for downstream compatible libraries.Alxandr commentedon Mar 9, 2015
It's (IMHO at least) better than disallowing default and named exports at the same time.
csnover commentedon Mar 10, 2015
Anders Hejlsberg (@ahejlsberg) I hear what you are saying about wanting to get everyone on board with the One True Module Format. I have that desire as well. However, I think I also share some of Aleksander Heintz (@Alxandr)’s concern that if TypeScript isn’t following the rules of that module format more closely that it’s going to cause problems as the “standard” ES6 module format does lots of non-standard things in TypeScript emitting to ES5.
In particular I definitely want people to be to experience the benefits of being able to have circular dependencies on modules with default values, which is currently not possible with the way AMD (and, in some ways, CJS) modules work. This is a fairly important feature when doing things like creating data models that have circular relations to other types, without either using an intermediate registry to retrieve types, or hanging values that should be defaults off of properties (the
var Foo = require('Foo').Fooanti-pattern, which TS would have to do, but at least it would be more hidden from developer eyes).I also understand and share the concern about emitting TS modules for down-level consumers that won’t know this One Weird Trick from Babel to support ES6 modules. I feel like continuing to support
export =syntax for this case might be OK since it’s basically an opt-in for the more restrictive default behaviour of legacy module formats. (Of course I don’t do much maintenance of the compiler so YMMV. :))Please let me know your thoughts on this if you have a moment, I’d like to have some holes poked in my thinking here. Thanks!
ahejlsberg commentedon Mar 10, 2015
Aleksander Heintz (@Alxandr) I think your suggestion has a lot of merit. Let me summarize what I think we would do.
If a module has only a default export, emit an assignment to
module.exports:Otherwise, emit everything as assignments to
exports.xxxand emit anexports.__esmodulemarker:On the import side, include an
__esmodulecheck on all default imports:It's not quite as pretty as what is emitted now, but I think it is worth it to get support for full ES6 module semantics down-level (as well as interop with modules emitted by Babel).
For an original import-equals declaration, we would give an error if the imported module has both regular exports and a default export (i.e. if it is an ES6 module). Such modules would only be importable using the new ES6 syntax.
Colin Snover (@csnover) With this proposal you'd be able to have circular dependencies between modules with default exports as long as the modules have at least one regular export as well (which could just be a dummy member).
Alxandr commentedon Mar 10, 2015
Anders Hejlsberg (@ahejlsberg) wouldn't it be possible to skip the fancy emit given metadata? I mean, typescript has typeinformation about everything (which is sort of the idea, right)? So if we know that the module being imported, we should know the format it exports at, right?
JsonFreeman commentedon Mar 10, 2015
Is it better to give an error for using import-equals to import an es6 module? Or is it better to emit an import-equals declaration in the same way as a default import? We could emit:
as
JsonFreeman commentedon Mar 10, 2015
Also, to make circular references work, don't you have to access the default member late? So instead of assigning the default to d eagerly, a call to
d.foo()would emit as_a.default.foo(). Why is this not the case, but for named exports it is?ahejlsberg commentedon Mar 10, 2015
Aleksander Heintz (@Alxandr) Yes, I think it would work to have the following rules:
module.exportsassignment when a module exports only a default, and emit everything as assignments toexports.xxxotherwise.requirereturns the default export object itself when importing a module that exports only a default, and assume everything is a property on the returned object otherwise.We would lose the ability to dynamically adapt on import based on the
__esModulemarker, but that would be ok as long as everyone else plays by the same rules.I suppose we'd still want to emit the
__esModulemarker such that Babel and other systems not guided by static type information can do the right thing.Jason Freeman (@JsonFreeman) I think we have two choices for import-equals with an ES6 (mixed) module. Either say it is an error (there's no backwards compatibility to worry about) or say that you get the module object with a set of properties including one named
default. The odd thing about the latter is that adding a regular export to a module that previously had only a default export would cause everything to "pop out" one level on the import-equals side. My personal inclination is to make import-equals an error with mixed modules.Regarding circular references, you're right, we'd want to rewrite references to the default import in the same way we'd do with any other import. Which in turn means we don't want the dynamic
_esModuleimport check. One more reason not to do it.57 remaining items
tomduncalf commentedon Nov 3, 2015
Sorry, I've just read the clarification at http://stackoverflow.com/questions/29596714/new-es6-syntax-for-importing-commonjs-amd-modules-i-e-import-foo-require/29598404#29598404 and it sounds like this scenario with ES6 target simply won't be supported until the modules in question update to the ES6 module standard. Bit of a shame but I can understand there are reasons for it.
mhegazy commentedon Nov 3, 2015
Tom Duncalf (@tomduncalf) i have a PR to fix your sample here: tomduncalf/express-es6-ts-example#1
You will need to build your code using
--target ES6with--module CommonJs. and import non-ES6 modules using the old syntaximport express = require("express");. The support for this was added in TS 1.7, so you need to usetypescript@nextfor now, until 1.7 is released within the next few weeks.tomduncalf commentedon Nov 3, 2015
That's awesome, thanks very much Mohamed - exactly what I was looking for!
Add missing ServerComponents to index.
Fix react addons update export style.
texastoland commentedon Apr 23, 2016
I believe this got implemented in 1.8 by
--allowSyntheticDefaultImportsin #5577.miguelcobain commentedon Jun 1, 2016
Sorry for bringing this up.
After reading this, I looks to me that it is impossible to to write a typings file that is compatible with both these import syntaxes:
and
Is this correct or did I get the wrong idea?
Most typings file out there use
export = somethinginstead of named exports, which I think is really unfortunate.To be more concrete, when I use this typings file: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/express/express.d.ts
I'd like to be able to write
import express, { Router } from 'express';and still be compatible with people that writeimport express = require('express');. What changes need to done to the typings file.Unfortunately most typings file don't have named exports which looks like a step backwards to me.
Thanks.
RyanCavanaugh commentedon Jun 2, 2016
Miguel Andrade (@miguelcobain) it sounds like what you actually want to do is compile with
--allowSyntheticDefaultImports? Or doesexpressactually have a member nameddefaultthat points to the containing object?basarat commentedon Jun 2, 2016
oh oh oh 🙋 I know the answer : no 🙅 🌹
miguelcobain commentedon Jun 3, 2016
Ryan Cavanaugh (@RyanCavanaugh) that option was what I was looking for. Many thanks!
tikhonandrey commentedon Aug 25, 2016
Hi!
I can suggest a compromise solution for projects with webpack.
es6 module default export will be fixed and bundle will be faster because babel use only one plugin
import React from 'react';and u doesn't need to change it to:
import * as React from 'react';try this
//file webpack.config.js
//file tsconfig.json
links:
ES6 modules with TypeScript and webpack
BABEL ES2015 modules to CommonJS transform
teyc commentedon Sep 18, 2016
Sorry - alm issue pls ignore
basarat commentedon Sep 18, 2016
Chui Tey (@teyc) That looks like an alm error. Feel free to create an issue there https://github.com/alm-tools/alm/issues but you will have to provide more information / reproduction steps 🌹