Skip to content

Change export = structurally to allow other exported elements in the same file#62103

Closed
weswigham wants to merge 3 commits intomicrosoft:mainfrom
weswigham:rearchitect-export-equals
Closed

Change export = structurally to allow other exported elements in the same file#62103
weswigham wants to merge 3 commits intomicrosoft:mainfrom
weswigham:rearchitect-export-equals

Conversation

@weswigham
Copy link
Copy Markdown
Member

To allow a export = to be typechecked alongside other exports in the module without the .cjsExportMerged layering we had going on. I've also deleted the "export = can't be in the same module as other exports" error, as, well, that was the point, but if we wanted to, we could always keep that error in ts source files... but, IMO, a more limited "value exports should not be lexically before a value export =" is actually the only error you need to prevent runtime problems with the usual cjs/bundler emit, and would be the better error.

This, in turn, should allow a cjs module.exports = to behave identically to a TS exports = (a goal for corsa), as now the later has all the functionality of the former.

I still need to go through the fourslash diffs and see what LS operations need to handle the export= symbol handling logic getting pushed up a layer (some quickfixes, for sure), but baseline-wise, these actually mostly look really good - this change makes it so modules always have their own symbol (and are directly never forwarded to another symbol - they just behave as though they are at times), and so can always be referred to by that symbol.

@weswigham weswigham requested a review from sandersn July 22, 2025 01:21
@typescript-bot typescript-bot added Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug labels Jul 22, 2025
*/
function mergeSymbol(target: Symbol, source: Symbol, unidirectional = false): Symbol {
if (target === source) {
return source; // target and source already merged (likely same symbol found in multiple export tables)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is only needed because I reused mergeSymbolTables to build the combined symbol table in resolveStructuredTypeMembers. If it wasn't to taste, I could always write a custom table merging function that handles this case outside of the core mergeSymbol.

}
}
}
if (mainModule.exports?.get(InternalSymbolName.ExportEquals) && moduleAugmentation.symbol.exports?.size) {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is almost a carbon copy of the export * logic above - the exception being that a local declaration doesn't implicitly "beat" something added by an export = - they have equal weight and get merged.

}
if (!symbol) {
if (isExportAssignmentAny(namespace)) {
return unknownSymbol; // no error - lookup on psuedo-`any` module
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously this was handled one level up the stack because the module itself was the any-typed const. Now it's not any (and can contain other well-typed members, like @typedefs), but it contains an export= that points at any, and we check that there to reenable the error-less lookup it used to have.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return unknownSymbol; // no error - lookup on psuedo-`any` module
return unknownSymbol; // no error - lookup on pseudo-`any` module

}
getSymbolLinks(merged).cjsExportMerged = merged;
return links.cjsExportMerged = merged;
return resolveSymbol(moduleSymbol, dontResolveAlias)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I could probably just delete resolveExternalModuleSymbol now - it's just an alias for resolveSymbol.

function getExportsOfModule(moduleSymbol: Symbol): SymbolTable {
const links = getSymbolLinks(moduleSymbol);
if (!links.resolvedExports) {
links.resolvedExports = emptySymbols
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getExportsOfModuleWorker can now recursively ask for it's own exports in cases like module.exports = exports - this short-circuits that, but still allows other things to merge into the cycle, despite the (sorta erroneous, sorta not) circular export.

extendExportSymbols(symbols, table);
}
}
symbols.delete(InternalSymbolName.ExportEquals)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This maintains the "the resolved exports don't contains an export= symbol" expectation most callers have. It's naturally still accessible on the raw, unresolved symbol.exports table for callers that need it, though.

type.constructSignatures = constructSignatures;
}

// And lastly, if the underlying symbol has an export=, we need to copy the type structure from it
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic and the getExportsOfModuleWorker should be identical, except the former is only concerned with copying values for the sake of symbolic traversal, while this copies up all type structure from the export= target - members, signatures, and index infos.

Comment on lines +34802 to +34803
const isPsuedoAnyModule = (parentSymbol && isExportAssignmentAny(resolveSymbol(parentSymbol)))
const isAnyLike = isTypeAny(apparentType) || apparentType === silentNeverType || isPsuedoAnyModule;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const isPsuedoAnyModule = (parentSymbol && isExportAssignmentAny(resolveSymbol(parentSymbol)))
const isAnyLike = isTypeAny(apparentType) || apparentType === silentNeverType || isPsuedoAnyModule;
const isPseudoAnyModule = (parentSymbol && isExportAssignmentAny(resolveSymbol(parentSymbol)))
const isAnyLike = isTypeAny(apparentType) || apparentType === silentNeverType || isPseudoAnyModule;

@typescript-bot
Copy link
Copy Markdown
Collaborator

With 6.0 out as the final release vehicle for this codebase, we're closing all PRs that don't fit the merge criteria for post-6.0 patches. If you think this was a mistake and this PR fits the post-6.0 patch criteria, please post to the 6.0 iteration issue with details (specifically, which PR and which patch criteria it satisfies).

Next steps for PRs:

  • For crash bugfixes or language service improvements, PRs are currently accepted at the typescript-go repo
  • Changes to type system behavior should wait until after 7.0, at which point mainline TypeScript development will resume in this repository with the Go codebase
  • Library file updates (lib.d.ts etc) continue to live in this repo or the DOM Generator repo as appropriate

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants