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

Importing defaults with es6 syntax doesn't work #5565

Closed
niba opened this issue Nov 8, 2015 · 22 comments
Closed

Importing defaults with es6 syntax doesn't work #5565

niba opened this issue Nov 8, 2015 · 22 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@niba
Copy link

niba commented Nov 8, 2015

Syntax: import name from "module-name"; doesnt work in my project with Typescript 1.6. The imported variable is undefined.

Example:

import createLogger from "redux-logger"; //createLogger is undefined

This syntax works

import createLogger = require("redux-logger");

@ahejlsberg
Copy link
Member

You need to write it as a namespace import (as opposed to a default import):

import * as createLogger from "redux-logger";

@niba
Copy link
Author

niba commented Nov 9, 2015

Thanks @ahejlsberg , I understand that the defaults syntax works in my TS internal files but it doesn't work when I want import something from node_modules (external modules)?

I have one more problem. When I do like you said
import * as createLogger from "redux-logger";
indeed in runtime createLogger keeps a createLogger() function but it is impossible to use it in my code because Typescript compiler doesn't allow me to do it.
For example:
`
import * as createLogger from "redux-logger";

...

applyMiddleware(createLogger()),`

this gives me an error error TS2349: Cannot invoke an expression whose type lacks a call signature.

What I'm doing wrong?

@ahejlsberg
Copy link
Member

Oh, I see. Then you need to either do this:

import * as reduxLogger from "redux-logger";

applyMiddleware(reduxLogger.createLogger());

Or this:

import { createLogger } from "redux-logger";

applyMiddleware(createLogger());

I suspect the latter one is what you're looking for.

@mhegazy mhegazy added the Question An issue which isn't directly actionable in code label Nov 9, 2015
@niba
Copy link
Author

niba commented Nov 9, 2015

@ahejlsberg In both cases I have undefined in my runtime. The only import which works is

const createLogger = require("redux-logger")

but with this syntax I don't have typescript support.

Redux-Logger library exports only one default function which is createLogger.

Update 1:

This one started working when I removed word "default" from function in my module definition

import createLogger = require("redux-logger");

@ahejlsberg
Copy link
Member

Are you using the SystemJS module loader? This looks like it might be the same problem as #5285 which has to do with SystemJS's "auto-magic" promotion of the default export.

@niba
Copy link
Author

niba commented Nov 9, 2015

@ahejlsberg I'm using commonjs module loader. I compared my compiled code with babel code and here are two versions:

Typescript:

var redux_logger_1 = __webpack_require__(678);

var test = redux_logger_1.default;

Babel:

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }

var _reduxLogger = require("redux-logger");

var _reduxLogger2 = _interopRequireDefault(_reduxLogger);

and in the runtime redux_logger_1 is a function and redux_logger_1.default is undefined. Babel code works because the condition inside function is true so it returns obj


To sum up what I've said earlier:
My goal is to import a node_module which exports one default function. In pure es6 javascript I would do that with import module from "module". In typescript I tried to do it in the same way with the same syntax but my module variable was undefined during runtime. What is more weird for me this syntax was working when I was trying to link one .ts file to another .ts file.

I discovered that Babel compiles default export to another syntax that Typescript and Typescript can't handle babel output. For example I have the following code:
export default function defaultFunction() {}
In babel it will be

exports.__esModule = true;
exports['default'] = defaultFunction;
module.exports = exports['default'];

In typescript

Object.defineProperty(exports, "__esModule", { value: true });
exports.default = defaultFunction

As I said the first output is incompatible with Typescript. The imports that don't work:
import defaultFunction from "module" - defaultFunction is undefined
import { defaultFunction } from "module" - It doesn't compile if the function is declared as default in definitions
import * as defaultFunction from "module" - calling defaultFunction() causes compiler error error TS2349: Cannot invoke an expression whose type lacks a call signature.
import defaultFunction = require("module"); - calling defaultFunction() causes compiler error error TS2349: Cannot invoke an expression whose type lacks a call signature.
const defaultFunction = require("module"); - works without typescript support but you can add casting to get some intellisense

With the second output I can just do import defaultFunction from "module"

@ahejlsberg
Copy link
Member

Ok, I think I see what is going on.

It appears the "redux-logger" module was transpiled with Babel. When Babel transpiles a module whose only export is an export default it injects a module.exports = exports["default"]; into the generated code, causing the exported object to become the function itself (instead of a module object that has the function as the default property). When paired with the _interopRequireDefault magic that Babel generates for imports the net effect is that a fake module object with a default property is created and the function can now be accessed as _reduxLogger2.default.

TypeScript doesn't do any of this magic (see here for more details). In order for TypeScript to consume the module you need to change the redux-logger.d.ts declaration file to actually reflect what is going on:

/// <reference path="../redux/redux.d.ts" />

declare module 'redux-logger' {

  function createLogger(options?: createLogger.ReduxLoggerOptions): Redux.Middleware;

  namespace createLogger {
    interface ReduxLoggerOptions {
      actionTransformer?: (action: any) => any;
      collapsed?: boolean;
      duration?: boolean;
      level?: string;
      logger?: any;
      predicate?: (getState: Function, action: any) => boolean;
      timestamp?: boolean;
      transformer?: (state:any) => any;
    }
  }

  export = createLogger;
}

Once you do that you should be able to import the module with a namespace import:

import * as createLogger from "redux-logger";

or with the equivalent:

import createLogger = require("redux-logger");

@niba
Copy link
Author

niba commented Nov 9, 2015

@ahejlsberg Thanks!! Works perfectly!

@mhegazy mhegazy closed this as completed Nov 9, 2015
@ahejlsberg
Copy link
Member

@niba Good to hear.

BTW, it appears that Babel is killing off the export default magic in their next release (babel/babel#2212). Once they do, and once redux-logger is re-transpiled, the redux-logger.d.ts declaration file on Definitely Typed will actually be correct. So, instead of using the modified declaration file you could fix redux-logger yourself by deleting the last line in node_modules/redux-logger/lib/index.js that reads

module.exports = exports["default"];

and continue to use the declaration file you already have. In that case your import should be:

import createLogger from "redux-logger";

@niba
Copy link
Author

niba commented Nov 10, 2015

@ahejlsberg Good to know that. Thank you for the information!

@theladyjaye
Copy link

theladyjaye commented Aug 18, 2016

I know this was closed a while ago, I just wanted to pop in an give a HUGE THANK YOU to @ahejlsberg for that explanation and remedy remove module.exports = exports["default"];.

You just made some things click for me with that. I literally wasted probably 4 hours trying to understand why my type definition, using export default was returning undefined. Then I stumbled upon export = Thing and was further scratching my head. Why on earth does import * as Thing from 'thing' work with export = Thing in my definition and then when I export default Thing I get undefined with import Thing from 'thing'. Babel.. that's why. hahahahaha oh man I am so glad I understand this now.

I would offer that it's probably worth adding to the documentation here:
https://typescript.codeplex.com/wikipage?title=Writing%20Definition%20%28.d.ts%29%20Files

As a special note for type definitions for Babel generated code.

@ahejlsberg
Copy link
Member

@aventurella Thanks. The complexities of ES6 module downlevel transpilation and interop is truly the gift that keeps on giving!

@LouisWayne
Copy link

Is this still a problem?

@theladyjaye
Copy link

It depends, for my money I have found that when I have an issue with a 3rd party lib I check to see if that lib was compiled with Babel. Usually it was so I just look at it's compiled source to see how it's exporting it and adjust my import accordingly

@LouisWayne
Copy link

@aventurella thanks for the quick response - can the problem be fixed by updating the typing file?

@theladyjaye
Copy link

The only way I have been able to "address" it is by understanding how the target 3rd party module has been exported. And you adjust things accordingly once you see how it's been exported by whatever transpiled it. Once you know that, if you are adding typings you just need to be sure you add them to describe the situation you observe in the 3rd party module.

@LouisWayne
Copy link

@aventurella - I really appreciate your detailed explanation - if you have a chance, could you give me a simple example please?

@theladyjaye
Copy link

What 3rd party module are you having an issue with?

@LouisWayne
Copy link

@aventurella - sorry for the late response, but here is the package that I use:

@types/quill: 1.3.0
quill: 1.3.1

The definition file (types/quill v1.3.0) looks like this: code link - note that there is no default export in the definition file.

And here is my code in *.ts file:

import { Quill } from 'quill';
new Quill('foo');

The compiled JS code is:

const quill_1 = require("quill");
new quill_1.Quill("foo");

The problem here is that quill_1.Quill is undefined - the compiled JS code should look like new Quill("foo") - so that it uses the external library properly but as you see, it is like quill_1.Quill("foo").

Now the definition file has been updated a few days ago, and it has the default export - but in v1.3.0, there is no default export. In this case, how can I fix the problem? Please advise...!!

@wy193777
Copy link

I encountered the same issue @niba was encountered with another library: cytoscape and it's type definition file. Actually I helped push the type definition to DefinitelyTyped, but I'm not so sure how to fix this after it's been merged.

When I import the module: import * as cytoscape from "cytoscape";, and call cytoscape(), compiler tell me Cannot invoke an expression whose type lacks a call signature. Type 'typeof "./node_modules/@types/cytoscape/index"' has no compatible call signatures.. But the compile result is right in this condition.

When I call with cytoscape.cytoscape(), ts compiler doesn't show an error but compile result is cytoscape_1.cytoscape() which is incorrect because the library supposed to be called directly.

cytoscape type source on DefinitelyTyped: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/d9ca7d7efff765abe162023a9aa47f51f473bfd7/types/cytoscape/index.d.ts

@aluanhaddad
Copy link
Contributor

@wy193777 the named export is the problem.
I believe you want

+export = cytoscape;
+export as namespace cytoscape;
- export function cytoscape(options?: cytoscape.CytoscapeOptions): cytoscape.Core;
+ function cytoscape(options?: cytoscape.CytoscapeOptions): cytoscape.Core;
- export function cytoscape(extensionName: string, foo: string, bar: any): cytoscape.Core;
+ function cytoscape(extensionName: string, foo: string, bar: any): cytoscape.Core;
- export namespace cytoscape {
+ namespace cytoscape {

based on the package's source code

Then you would import it via either

import cytoscape = require('cytoscape');

or

import cytoscape from 'cytoscape'; // --allowSyntheticDefaultImports true

or referencing the global when not using modules, depending on your environment.

@wy193777
Copy link

Thanks @aluanhaddad. I just submitted a new pull request to fix it.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

7 participants