Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ES6 Modules default exports interop with CommonJS #2719

Closed
teppeis opened this issue Apr 11, 2015 · 37 comments
Closed

ES6 Modules default exports interop with CommonJS #2719

teppeis opened this issue Apr 11, 2015 · 37 comments
Labels
Committed The team has roadmapped this issue Fixed A PR has been merged for this issue Suggestion An idea for TypeScript

Comments

@teppeis
Copy link

teppeis commented Apr 11, 2015

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.

@ahejlsberg
Copy link
Member

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.

@ahejlsberg ahejlsberg added the Suggestion An idea for TypeScript label Apr 13, 2015
@mizchi
Copy link

mizchi commented Apr 13, 2015

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.

@ahejlsberg
Copy link
Member

@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.

@teppeis
Copy link
Author

teppeis commented Apr 13, 2015

@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.

@vvakame
Copy link
Contributor

vvakame commented Apr 14, 2015

👍

4 similar comments
@niemyjski
Copy link

+1

@scottwio
Copy link

+1

@okunokentaro
Copy link

+1

@EddyLane
Copy link

EddyLane commented Jun 4, 2015

+1

@mhegazy mhegazy added the In Discussion Not yet reached consensus label Jun 4, 2015
@vladkosinov
Copy link

+1

@RyanCavanaugh RyanCavanaugh added Help Wanted You can do this Committed The team has roadmapped this issue and removed In Discussion Not yet reached consensus labels Jun 8, 2015
@benlesh
Copy link

benlesh commented Jun 10, 2015

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?

@chicoxyzzy
Copy link
Contributor

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

@benlesh
Copy link

benlesh commented Jun 11, 2015

@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)

@guybedford
Copy link
Contributor

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.

@benlesh
Copy link

benlesh commented Jun 11, 2015

@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

@ghost
Copy link

ghost commented Jun 17, 2015

Must have feature.

@almilo
Copy link

almilo commented Jun 24, 2015

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

@tinganho
Copy link
Contributor

tinganho commented Jul 8, 2015

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'

@trusktr
Copy link

trusktr commented Jan 27, 2017

@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.

@ORESoftware
Copy link

ORESoftware commented Mar 24, 2017

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

@trusktr
Copy link

trusktr commented Mar 26, 2017

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).

@trusktr
Copy link

trusktr commented Mar 26, 2017

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.

@ORESoftware
Copy link

@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

@emirotin
Copy link

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.

@aluanhaddad
Copy link
Contributor

aluanhaddad commented Mar 30, 2017

@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);

@emirotin
Copy link

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

@FallenMax
Copy link

FallenMax commented Jun 26, 2017

@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:

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

Thanks for any idea!

@kitsonk
Copy link
Contributor

kitsonk commented Jun 26, 2017

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.

@FallenMax
Copy link

@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!

bryphe pushed a commit to onivim/oni that referenced this issue May 16, 2018
* Add middle click tab closing (#2069)

by adding a onMouseDown directive to both tab file icon and
tab name and checking the resulting React.MouseEvent for a
middle click

* Fix unused imports and whitespace in Tabs ui test (#2069)

* Update Tabs component test snapshot (#2069)

* Fix Tabs ui test to correctly retrieve children (#2069)

* Differentiate between single und multiple tabs in test (#2069)

* Use mousedown event for tab selection and closing (#2069)

by middle clicking and let the tab itself handle the logic
for it by checking React.MouseEvent.button value

* Update classnames dependency to lastest master branch

and write own @types module declaration so that it can be used in
testing nstead of using a mock. The mock does not suffice as the
way the actual module previously had to be imported was
"import * as classNames" where classNames was the actual function.

It is not possible to build a module in typescript/es6 imports, which
will directly return a function in the same way the dependency did
in commonjs module syntax. Instead when defining a function to be
returned as default export it is returned as an Object like this
"{ default: [Function] }", which is correctly resolved when importing
with "import classNames from 'classnames'".

This only previously worked in production as webpacks ts-loader,
handles this issue, whereas when testing the sources are only compiled
with tsc.

There is an update to the classnames dependency on the current master,
but there hasn't been a release since 2006. So options were to setup
webpack for tests as well or add updated classnames dependency which
sets a "default" value on its commonjs exports.

Links for reference:
JedWatson/classnames#152
JedWatson/classnames#106
DefinitelyTyped/DefinitelyTyped#25206
microsoft/TypeScript#2719

* Fix tab click onMouseDown callback

* Test tab clicks to select/close trigger callbacks

* Mock child react components directly to test smaller unit

* Reset calls to callback mocks in each test case

* Add tests for tabs interaction with FileIcon/Sneakable
@microsoft microsoft locked and limited conversation to collaborators Jun 18, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Committed The team has roadmapped this issue Fixed A PR has been merged for this issue Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests