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

Types are not imported properly for packages that use exports.default. #25854

Closed
zavr-1 opened this issue Jul 21, 2018 · 14 comments · Fixed by #27208
Closed

Types are not imported properly for packages that use exports.default. #25854

zavr-1 opened this issue Jul 21, 2018 · 14 comments · Fixed by #27208
Assignees
Labels
Bug A bug in TypeScript Needs More Info The issue still hasn't been fully clarified

Comments

@zavr-1
Copy link

zavr-1 commented Jul 21, 2018

TypeScript Version: 3.1.0-dev

Search Terms: type export default

Code and Actual Behaviour

Scenario 1 (perfect): export default, import alias

tt.js

/**
 * Run this function
 * @param {string} t
 */
export default function testFunction(t) {
  return t
}

t.js

import t from './tt'

t
ScreenshotResult

test

The preview shows correctly.

Scenario 2 (Bug): exports.default, import originalName

tt.js (compiled w/ babel)

Object.defineProperty(exports, '__esModule', {
  value: true,
})
exports.default = testFunction

/**
 * Run this function
 * @param {string} t
 */
function testFunction(t) {
  return t
}

t.js

import testFunction from './tt'

testFunction(
ScreenshotsResult

test

test

There's no description on hover, but it comes up when starting to type arguments. But this is only because it's got the same name so VS Code looks up by the name. Proof: if function is not exported, it will still show description. Therefore, same as case 3.

Scenario 3 (Bug): exports.default, import alias

tt.js (compiled w/ babel)

Object.defineProperty(exports, '__esModule', {
  value: true,
})
exports.default = testFunction

/**
 * Run this function
 * @param {string} t
 */
function testFunction(t) {
  return t
}

t.js

import t from './tt'

t(
ScreenshotsResult

test

test

There's no description on hover, it does not come up when starting to type.

Expected behavior: Should always work as scenario 1.

So it basically means that all modules transpiled with babel must be imported using their original name? And even then it's only because it has got the same name.

Consider having 2 files:

tt.js

exports.default = testFunction

/**
 * Run this function (test 1)
 * @param {string} t1
 */
function testFunction(t1) {
  return t1
}

and

tt2.js

exports.default = testFunction

/**
 * Run this function (test 2)
 * @param {string} t2
 */
function testFunction(t2) {
  return t2
}

and a file in which I want to use them:

t.js

import testFunction from './tt'
import testFunction2 from './tt2'

/** @type {typeof import('./tt2')} */
testFunction2()

I won't be able to ever see type of testFunction2 even using typeof {import}? Unless I do module.exports = testFunction in tt2.js which Babel never does (and there's no setting for it). I think TypeScript should parse exports.default = in the same way as module.exports = as babel@5 did export then this way, whereas babel@6 doesn't.

https://github.com/59naga/babel-plugin-add-module-exports#readme

I can't even use the above plugin because even though it adds module.exports = exports.default; VS Code does not assign the type to module.exports.

@mhegazy mhegazy added the Bug A bug in TypeScript label Jul 23, 2018
@mhegazy mhegazy assigned ghost Jul 23, 2018
@mhegazy mhegazy added this to the TypeScript 3.1 milestone Jul 23, 2018
@ghost
Copy link

ghost commented Jul 26, 2018

@zavr-1 This seems to be working for me.

exports_default

@ghost ghost added the Needs More Info The issue still hasn't been fully clarified label Jul 26, 2018
@zavr-1
Copy link
Author

zavr-1 commented Aug 2, 2018

@andy-ms doesn't work for me in the insiders on mac

Version 1.26.0-insider (1.26.0-insider)

screen shot 2018-08-02 at 22 59 14

@ghost
Copy link

ghost commented Aug 2, 2018

@zavr-1 So what do you see when hovering over g? If typescript was crashing you would at least see Loading.... It's not clear what's not working in that picture.

EDIT: Never mind, got a repro after deleting tsconfig.json. Strange.

@ghost
Copy link

ghost commented Aug 2, 2018

OK, reproduced this bug. It happens because --allowSyntheticDefaultImports is on by default in inferred projects.

/// <reference path='fourslash.ts'/>

// @allowJs: true
// @allowSyntheticDefaultImports: true

// @Filename: /a.js
////exports.default = f;
/////**
//// * Run this function
//// * @param {string} t
//// */
////function f(t) {}

// @Filename: /b.js
////import f from "./a"
/////**/f

verify.quickInfoAt("", "?", "Run this function"); // No documentation

@weswigham Could you take a look?

@weswigham
Copy link
Member

weswigham commented Aug 3, 2018

import f from "./a" refers to the entire module of a.js under allowSyntheticDefaultImports and not its default. There's no __esModule marker in your example, so that's a correct inference of what will happen at runtime given the import helper, too. (The example in the OP has one, but it's defined such that we don't recognize it (we should try to recognize it), which is why it behaves the same way).

A synthetic default always overrides a real one unless the __esModule marker is found. (Or the imported file has real esmodule syntax, at which point it also won't have a synthetic default)

@weswigham
Copy link
Member

For reference, if you edit the output to have an __esModule marker that we can find, it behaves like you would like:

/// <reference path='fourslash.ts'/>

// @allowJs: true
// @allowSyntheticDefaultImports: true

// @Filename: /a.js
////exports.__esModule = true;
////exports.default = f;
/////**
//// * Run this function
//// * @param {string} t
//// */
////function f(t) {}

// @Filename: /b.js
////import f from "./a"
/////**/f

verify.quickInfoAt("", `(alias) (property) f: (t: string) => void
import f`, "Run this function"); // Passes

@weswigham
Copy link
Member

We should probably recognize

Object.defineProperty(exports, 'name', {...});

at the top level of a file as a special assignment, since our emit when targeting es5 and babel's current default emit now use such a call for the marker over a straight assignment.

@sandersn You know how doable this is?

@weswigham
Copy link
Member

weswigham commented Aug 3, 2018

@zavr-1 As a workaround, if you use babel with the loose option, you can get it to emit the marker in a form we can understand. (Specifically only loose for babel-plugin-transform-es2015-modules-commonjs is required to get the output to change)

@zavr-1
Copy link
Author

zavr-1 commented Aug 4, 2018

@weswigham alright thanks, the loose option did the trick! it also works for babel-plugin-transform-modules-commonjs which I guess is the same thing as babel-plugin-transform-es2015-modules-commonjs.

@sandersn
Copy link
Member

sandersn commented Aug 6, 2018

@weswigham I have seen this pattern around and agree that it would be useful to have. I can't tell from this discussion whether it needs a new issue to track it or not. Do you think so? If not, we should change the title to say exactly that.

Adding the pattern is not too difficult, but a lot more wordy than existing patterns. You'd have to

  1. Put a special case in the binder when binding call expressions, probably, to check for expression: Object.defineProperty, parameters[0] = exports, parameters[1] = StringLiteral. You then need to decide where to mark the declaration for consumption in the checker. Probably on exports?
  2. Put some special-case code in the checker to retrieve the type of exports.name. checkCallExpression should already do the right thing as far as error checking.

I would note that roundtripping our emit is not a goal and I am tempted to make it a non-goal. I'm not convinced roundtripping Babel's emit should be a goal either, but humans also write this pattern, so I'd class it as Nice to Have right now.

@weswigham
Copy link
Member

weswigham commented Aug 6, 2018

We can make this issue track it - we probably need to edit the title and the OP a bit to make the lede not burried in the comments, however.

@zavr-1
Copy link
Author

zavr-1 commented Aug 10, 2018

@weswigham it doesn't actually work even if you set loose and define exports.__esModule = true.

exports.__esModule
exports.default = testFunction

/**
 * Run this function
 * @param {string} t
 */
function testFunction(t) {
  return t
}

screen shot 2018-08-10 at 01 26 32

What you have done in your fourslashes is named function f and imported it as f, which is the case 2 in the bug report -- is only showing because of global type inference of whatever it is called.

@weswigham
Copy link
Member

Hm. In our test harness, this:

/// <reference path='fourslash.ts'/>

// @allowJs: true
// @allowSyntheticDefaultImports: true

// @Filename: /a.js
////exports.__esModule = true;
////exports.default = f;
/////**
//// * Run this function
//// * @param {string} t
//// */
////function f(t) {}

// @Filename: /b.js
////import q from "./a"
/////**/q

verify.quickInfoAt("", `(alias) (property) q: (t: string) => void
import q`, "Run this function"); // Passes

Still works fine (though I've only checked on latest master TS, so it may have been recently fixed)? @andy-ms know anything?

@weswigham
Copy link
Member

Ahhh @zavr-1 your example seems to be missing an actual assignment to exports.__esModule

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Needs More Info The issue still hasn't been fully clarified
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants