-
Notifications
You must be signed in to change notification settings - Fork 12.9k
Improve import type support for commonjs exports #49745
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
Conversation
This PR makes getTypeFromImportTypeNode a little more like getExternalModuleMember: for JS files, it now uses both `getTypeOfSymbol` and `getExportsOfSymbol`, and uses whichever one returns a symbol. This allows using arbitrary properties of a CJS export= as types in JSDoc; previously a special case in the binder enabled only CJS export= where all properties were shorthand assignments. Fixes #49195
a: import('./mod').Thing, | ||
~~~~~ | ||
!!! error TS2694: Namespace '"tests/cases/conformance/salsa/mod".export=' has no exported member 'Thing'. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, this is different from what you’d get if mod.js had said module.exports.Thing = Thing
, right? I guess I was under the impression that we intended to bind the object literal property assignment similarly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that's a good point. IIRC this is an existing limitation of the code, but I'll take another look to see how to fix it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So: currently the code has special lookup for object literal property assignments, but it doesn't try to follow value-only symbols that come from the properties as if they were aliases, which is what is needed to make this work. Either that, or the binder needs to bind the object literals so that the property assignments have symbols that are marked as aliases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is existing alias lookup code for property assignments, so it might not be too difficult to add this feature. This isn't a high-priority thing so I may try to add it and see where it goes.
} | ||
|
||
function values( | ||
a: typeof import('./mod').Thing, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly, this sure looks like it would be referring to the constructor type of Thing
, but it’s the instance type here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, it's the constructor type. See the types baslines at https://github.com/microsoft/TypeScript/pull/49745/files#diff-88e8eb66de54881e4ab7d48ec9f25031a7327dc04e169d0f32347b2b49d22068R133-R134
Also it's in a TS file so the new JS rule doesn't apply.
} | ||
==== tests/cases/conformance/salsa/main.js (0 errors) ==== | ||
/** | ||
* @param {import("./mod").Thing} a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens when you do typeof
in the JS file? Is it possible to get the constructor type?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. I added a test for that.
1. Bind property assignments same as shorthand property assignments in module.exports object literal assignments. 2. Bind all such assignments, even if the object literal contains non-property assignments. This is different from before, and it requires slightly smarter code to prefer aliases when checking CJS imports.
The new commit improves the binder for property assignments with class expressions and identifiers. Read the commit text for full details. |
Currently the breaking change is that, previously, a conflicting Before: /** @typedef {number} C */
module.exports = {
C: class { }
} would previously import as: import { C } from './mod'
var c: C = 12
c = new C() // error, 'C' not assignable to 'number' and now imports as import { C } from './mod'
var c: C = 12 // error 'number' not assignable to 'C'
c = new C() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comments from yesterday's discussion
src/compiler/binder.ts
Outdated
* Note that constructor functions are not considered aliasable in the binder, but are in the checker -- because identifying | ||
* a constructor function requires a complete binding, because property declarations are non-local. | ||
*/ | ||
function isAliasablePropertyAssignment(p: ObjectLiteralElementLike) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe should just bind everything and let the checker resolve straight to the property assignment's initialiser
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Later: I tried this (except for binding non-aliases as properties) and decided that to do this correctly, export=
would need to change to work the same way.
I'm not sure what to do about this, but I think I managed to convince myself that it is equivalent to this typescript, which also fails to include the typed meaning of classes, except in import types: // @file: mod.ts
class Thing { x = 1 }
export = { Thing }
// @file: index.ts
import { Thing } from './mod'
declare const thing: Thing This gives two errors:
(2) is the same problem as we encountered in JS for Treating I tried taking the esmoduleInterop advice: // @esmoduleinterop: true
// @file: mod.ts
class Thing { x = 1 }
export = { Thing }
// @file: index.ts
import mod from './mod'
declare const thing: mod.Thing But this tells me (correctly?) that |
The bottom line is: I got pretty close to making |
Just include the original fix
This PR makes getTypeFromImportTypeNode a little more like getExternalModuleMember: for JS files, it now uses both
getTypeOfSymbol
andgetExportsOfSymbol
, and uses whichever one returns a symbol. This allows using arbitrary properties of a CJSexport=
as types in JSDoc; previously a special case in the binder enabled only CJSexport=
where all properties were shorthand assignments.Fixes #49195