Typescript: ES6 Modules default exports interop with CommonJS

Created on 11 Apr 2015  ·  37Comments  ·  Source: microsoft/TypeScript

CommonJS cannot requires a package transpiled from TypeScript ES6 Modules using default export well.

TS 1.5-alpha transpiles default exports:

// foo.ts
export default foo;

to:

// foo.js ("main" in this package.json)
exports.default = foo;

So users of this package foo need to add .default property to require().

var foo = require('foo').default;

It's a little messy...

Babel resolves this issue.

http://babeljs.io/docs/usage/modules/#interop

Interop

In order to encourage the use of CommonJS and ES6 modules, when exporting a default
export with no other exports module.exports will be set in addition to exports["default"].

export default test;
exports["default"] = test;
module.exports = exports["default"];

If you don't want this behaviour then you can use the commonStrict module formatter.

It sounds pretty nice and actually works for me.

Committed Fixed Suggestion

Most helpful comment

@emirotin If you want to export types, you should consider placing them in a merged namespace. Here is an example
configure.ts

export = configure;

function configure(options: configure.Options) {
  return { ...options, key: 'value' };
}

namespace configure {
  export interface Options {
    pluginName: string;
    inject?: true;
  }
}

example-consumer.ts

import * as configure from './configure'; // or import = require ..., or import configure from...

const options: configure.Options = {
  pluginName: 'abstraction helper gizmo'
};
configure(options);

All 37 comments

We considered that, but it only works if you also emit an __esModule marker property and emit a dynamic check for such a marker on every default import (which is what Babel does). Since you can already get the desired effect with TypeScript's existing export =, and since you can import that with the existing import x = require(...) syntax, we felt that it was best to stick with pure ES6 module semantics and keep the code generation as clean as possible.

Adding a bit more detail, Babel's compatibility mode generates a default export as an assignment to module.exports only when the module exports nothing but a default. The minute you add any other export, the default export now "snaps back" and becomes an export named default. Hence you need an __esModule marker to indicate which mode the module is in, and you need a dynamic check of that marker on every import. It gets rather messy.

This behaviour is my blocker to adopt 1.5.
I expected module.exports = with --module commonjs when I use export default but TypeScript didn't so.

export default class Foo {}

commonjs users have to write below now with 1.5. (not for typescript, raw js for node.js)

let Foo = require('./foo').default

But this problem may be common.js/require spec's...

*_added *_ I wrote typescript as a part of my project, and provide common.js module to be required.

@mizchi You're right, if you use export default then CommonJS consumers will have to access the default property. This is the one big difference between default exports in ES6 and assigning to module.exports in CommonJS. There is really no clean way to solve this problem. Babel supports a compatibility mode and a magic __esModule marker that you dynamically have to check, but I'm not convinced that's any better. My feeling is we're better off just going with pure ES6 semantics.

@ahejlsberg yes, there is no clean way. I see your policy for interoperability with CommonJS.

So, how about interop with other ES6 Module transpilers?
Babel, Traceur, SystemJS and some others cannot import a module from TypeScript 1.5-alpha as default properly.

// foo.ts (main of package "foo")
export default 'foo';
// An ES6 module to be compiled by Babel, Traceur, SystemJS
import foo from 'foo';
console.log(foo); // { default: 'foo' } instead of just 'foo'

If TypeScript sets exports.__esModule = true as a transpiled ES6 module, they import it as a default.

:+1:

+1

+1

+1

+1

+1

I'm trying to redevelop RxJS in TypeScript, and this is a requirement, as RxJS needs to support CJS very cleanly. Any idea of where this sits as a priority?

this is must-have feature. our team is really missing it and this is a stopper.

@chicoxyzzy Thus far my solution is to transpile to es6, then use Babel to get to AMD and CJS. Finally Browserify to create a global, bundled file. It's a little wonky, but it works. (I'm authoring a library though)

Just to clarify on the interop discussion - Babel's handling of making the default export the primary module.exports is a convenience unrelated to Babel's handling of CommonJS interop - they are orthogonal concerns. The handling of CommonJS interop is exactly the flag system though.

@guybedford nonetheless, if you want CJS interop without having to add __esModule all over your code, _for now_ the answer is tsc -> es6 then babel -> cjs

Must have feature.

While evaluating TS for a new big project I am working on, we stumbled upon this issue which is a blocker.
See: https://github.com/almilo/ts-es6-interop-issue

I'm not sure why everyone is +1:ing. Either everyone is TypeScript library owners or they think it solves the opposite problem.

I'm actually more annoyed about the opposite problem. How to import a default export from CommonJS in ES6. I want to write everything using the ES6 module syntax and not having to resolve to import module = require('./module').

Many libraries have a default exported function defined in CommonJS like below:

module.exports = function() { ... }

And the best solution is to use the old TS syntax to import;

import module_ = require('./module');

Because you can't import it using ES6 module syntax. So my code base is filled with mixed module syntaxes. If we decided to emit the __esModule marker. Why can't we go the whole way and emit a check for the marker too?

Then my code base can be a lot cleaner:

import something from 'commonjs'

thanks @vvakame!

Thanks! Conclusion:

  • default exports interop with CommonJS (https://github.com/Microsoft/TypeScript/issues/2719#issue-67722721): won't fix
  • default exports interop with other ES6 transpilers like Babel (https://github.com/Microsoft/TypeScript/issues/2719#issuecomment-92368965): fixed #3586

ah, I missed it.

@mhegazy
now, TypeScript's es6 module syntax is not support export = style export assignment.
We can't replace external module syntax to es6 module syntax in .d.ts file.
I have a question. whether or not to write es6 module syntax definition for CommonJS code?

I wrote a blog post on interop between CommonJS and ES6 modules in TypeScript. It has a section on this issue that I thought might be useful to some of the people here (and future people who find this issue) so I thought I'd share. It's basically the TS -> ES6 -> Babel -> CJS solution but actually shows a brief example of how to do it using webpack.

http://www.jbrantly.com/es6-modules-with-typescript-and-webpack/#packagingasalibrary

@jbrantly thanks for the article. It was very useful.

@omidkrad @jbrantly as a side note, the information in the article is not actual anymore - Babel has removed the ability to export the default as the module in v6. So that doesn't work.

Does anyone know any other way of exporting a TypeScript library to JS without the need for default? I have a library I'd like to make public via npm, and I'm still doing:

var MyClass require('my-class').default;
var myClass = new MyClass();

But it's kind of cumbersome if you expect other people to use it. It's unexpected to most users.

A pattern we have been using to make it slightly better for people not consuming your packages via TypeScript would be to create a module the re-exports your exposed APIs. Making this the main module of the package would simplify imports by others.

For example:

import MyClass from 'my-class';

export {
  MyClass
}

And then MyClass would be available on var MyClass = require('my-package').MyClass.

Following on from that you can export a function directly (tested with 1.8)

import {default as hello} from "./hello"; export = hello;

You do need to be sure to use the require syntax when importing, but this seems to compile fine, and you can mix these up with es6 imports
import test = require("ts-npm-sample"); import {SomeActionable} from "./dependency"; ... etc

Sample here

@ahejlsberg

since you can import that with the existing import x = require(...) syntax, we felt that it was best to stick with pure ES6 module semantics and keep the code generation as clean as possible.

Actually, import x = require(...) isn't "pure ES6 semantics" from an end-user point of view, it's only ES6 semantics from an implementation point of view.

In other words, there's more care being placed into the implementation than into how end users interact with modules. Why don't we just make it easy for the end user?

It would be great to have a way to configure import default to just be the export object of the module being imported. For example, this would work excellently well in cases where all modules are define modules.

I don't see why this has to be a pain point. It would be so great to have it be configurable!

@tinganho

And the best solution is to use the old TS syntax to import;

import module_ = require('./module');

That might be only true in purely TypeScript environments. But you might know that TypeScript can compile to es2015 and leave es6 module statements alone. Will this cause another loader like webpack or babel to crash? If so then that's no good.

However, if we can configure default imports to grab the exported objects of AMD or CommonJS modules in TS, then we can for example take advantage of Babel's interop in a following build step, and everything will just work.

Hey all, as a newcomer to this thread, I am looking for "the answer" here. My thinking is that this has been resolved in favor of the askers, but I am not sure.

I have a TS module that does this:

export default {foo: 'bar'};

in my Node.js version 0.12 code, I have

var x = require('ts-module').default;

how exactly, can I avoid using the default property when using older versions of Node?

in other words, I cannot do this:

const {default as x}  = require('ts-module');

because my library needs to support older versions of Node.

It would be nice to know in simple terms what the resolution to this issue was. Thanks all.

I know that AVA solved the problem this way

https://github.com/avajs/ava/blob/master/lib/main.js

like so:

module.exports = runner.test;

// TypeScript imports the `default` property for
// an ES2015 default import (`import test from 'ava'`)
// See: https://github.com/Microsoft/TypeScript/issues/2242#issuecomment-83694181
module.exports.default = runner.test;

but I am not sure if they generated that file with TS though...

I am looking for the way to generate/declare that kind of code with TS

The official solution is to use TypeScript's special syntax, for example:

export = {foo: 'bar'};

then in your old Node code you can do

var x = require('ts-module');

without default.

However, this has serious limitations when trying to interop with other module formats or module bundlers because (for example) TypeScript compiles those statements to nothing (they just disappear), and then you'll get errors in (for example) Webpack.

That special format basically only works for app authors, not for library authors, and not for app authors doing more complex things like needing intermediate module processing steps (tools like Babel, Webpack, etc, don't understand TypeScript's special export =/import =format, and when you compile to the format that they understand then TypeScript just deletes the statements).

Basically the downside of ES6 is that it isn't designed to be backwards compatible with previous module formats, and there isn't any standard on how to transpile ES6 modules for older formats.

@trusktr

thanks, yeah that solution unfortunately won't work for me, because I am exporting another TS interface from the same file, so TS will complain when I useexport =syntax:

screenshot 2017-03-26 17 17 00

Maybe there is no solution to this one at the moment :(

my personal solution is to do this:

const x = {foo:'bar'}
x.default = x;
module.exports = x;

not my fav thing in the world LOL

Have the similar problem. I'm writing typedef file for an existing module. The module is CommonJS that exports a function (module.exports = function () {})

Alongside with it I also have to define and export some interfaces.

So the export = syntax works but only until I want to also export the types.

@emirotin If you want to export types, you should consider placing them in a merged namespace. Here is an example
configure.ts

export = configure;

function configure(options: configure.Options) {
  return { ...options, key: 'value' };
}

namespace configure {
  export interface Options {
    pluginName: string;
    inject?: true;
  }
}

example-consumer.ts

import * as configure from './configure'; // or import = require ..., or import configure from...

const options: configure.Options = {
  pluginName: 'abstraction helper gizmo'
};
configure(options);

Right @aluanhaddad thanks a lot, have just discovered it 15 minutes ago :)

@aluanhaddad Thanks for solution!

What if I need to write a module that exports an object, and exports some interfaces (for ts dependencies)? like:

export interface Option{
 key1:string
}
export = {
  fun(option:Option) {
    // do stuff
  }
}

Some annoying limitations:

  • I'm now incrementally integrating TypeScript to an existing js project, so interop with other commonjs modules is important (i.e. we avoid export default).

  • The exported object is sort of an instance, so I rather not use separate exports like export function fun(){}

  • The namespace method also doesn't work here because:

  • exported object may not have a name
  • we can't export object & namespace with same name

Thanks for any idea!

You just need to name the exported object:

const exportObject = {
  fun(option:exportObject.Option) {
    // do stuff
  }
}

export = exportObject;

namespace exportObject {
    export interface Option{
        key1: string
    }
}

But of course when you use the top level export, it exports an object anyways. So why wouldn't you do this?

export interface Option{
 key1:string
}
export function fun(option:Option) {
  // do stuff
}

As that would have exactly the same shape of an export as what you originally had.

@kitsonk
The namespace solution works and satisfies my need. I didn't know so well about namespace :)
The export separately solution is not desirable here, as my intention is to export an instance which contains methods and properties. Separate exports will "break" this instance and make its properties read-only.

My current approach is to just export this instance as a named object (export const instance = {}).

Thanks a lot!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

fwanicka picture fwanicka  ·  3Comments

DanielRosenwasser picture DanielRosenwasser  ·  3Comments

uber5001 picture uber5001  ·  3Comments

blendsdk picture blendsdk  ·  3Comments

siddjain picture siddjain  ·  3Comments