diff --git a/tsc/internal/checker/emitresolver.go b/tsc/internal/checker/emitresolver.go index f0058eb2b28e3..d20134e64612f 100644 --- a/tsc/internal/checker/emitresolver.go +++ b/tsc/internal/checker/emitresolver.go @@ -3,6 +3,7 @@ package checker import ( "maps" "slices" + "strings" "sync" "github.com/microsoft/TypeScript/tsc/internal/ast" @@ -10,6 +11,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/evaluator" "github.com/microsoft/TypeScript/tsc/internal/jsnum" + "github.com/microsoft/TypeScript/tsc/internal/modulespecifiers" "github.com/microsoft/TypeScript/tsc/internal/nodebuilder" "github.com/microsoft/TypeScript/tsc/internal/printer" ) @@ -127,6 +129,96 @@ func (r *EmitResolver) IsDeclarationVisible(node *ast.Node) bool { return r.checker.isDeclarationVisible(node) } +func (r *EmitResolver) IsUniqueESSymbol(symbol *ast.Symbol) bool { + if r.checker.getTypeOfSymbol(symbol).flags&TypeFlagsUniqueESSymbol != 0 { + return true + } + for _, declaration := range symbol.Declarations() { + file := ast.GetSourceFileOfNode(declaration) + if file != nil && file.Symbol != nil { + if exported := file.Symbol.Exports()[symbol.Name()]; exported != nil && r.checker.getTypeOfSymbol(exported).flags&TypeFlagsUniqueESSymbol != 0 { + return true + } + } + } + return false +} + +func (r *EmitResolver) IsNameResolvableToSymbol(location *ast.Node, name string, target *ast.Symbol) bool { + resolved := r.checker.resolveName(location, name, ast.SymbolFlagsValue|ast.SymbolFlagsType|ast.SymbolFlagsNamespace, nil, false, false) + if resolved == nil { + return false + } + if resolved.Flags()&ast.SymbolFlagsAlias != 0 { + resolved = r.checker.GetAliasedSymbol(resolved) + } + for _, declaration := range target.Declarations() { + if slices.Contains(resolved.Declarations(), declaration) { + return true + } + } + return false +} + +func (r *EmitResolver) GetModuleSpecifierForSymbol(symbol *ast.Symbol, enclosingDeclaration *ast.Node) string { + // Symbol tracker calls occur while the checker lock is already held. + contextFile := ast.GetSourceFileOfNode(enclosingDeclaration) + moduleSymbol := symbol.Parent() + if contextFile == nil || moduleSymbol == nil { + return "" + } + resolutionMode := r.checker.program.GetDefaultResolutionModeForFile(contextFile) + endingPreference := modulespecifiers.ImportModuleSpecifierEndingPreferenceNone + if resolutionMode == core.ResolutionModeESM { + endingPreference = modulespecifiers.ImportModuleSpecifierEndingPreferenceJs + } + result := modulespecifiers.GetModuleSpecifiers( + moduleSymbol, + r.checker, + r.checker.compilerOptions, + contextFile, + r.checker.program, + modulespecifiers.UserPreferences{ + ImportModuleSpecifierPreference: modulespecifiers.ImportModuleSpecifierPreferenceProjectRelative, + ImportModuleSpecifierEnding: endingPreference, + }, + modulespecifiers.ModuleSpecifierOptions{OverrideImportMode: resolutionMode}, + false, + ) + if len(result.Specifiers) == 0 { + return "" + } + specifier := result.Specifiers[0].AsString() + if (r.checker.compilerOptions.GetModuleResolutionKind() == core.ModuleResolutionKindNode16 || r.checker.compilerOptions.GetModuleResolutionKind() == core.ModuleResolutionKindNodeNext) && strings.Contains(specifier, "/node_modules/") { + swappedMode := core.ModuleKindESNext + if r.checker.program.GetEmitModuleFormatOfFile(contextFile) == core.ModuleKindESNext { + swappedMode = core.ModuleKindCommonJS + } + endingPreference := modulespecifiers.ImportModuleSpecifierEndingPreferenceNone + if swappedMode == core.ModuleKindESNext { + endingPreference = modulespecifiers.ImportModuleSpecifierEndingPreferenceJs + } + alternate := modulespecifiers.GetModuleSpecifiers( + moduleSymbol, + r.checker, + r.checker.compilerOptions, + contextFile, + r.checker.program, + modulespecifiers.UserPreferences{ + ImportModuleSpecifierPreference: modulespecifiers.ImportModuleSpecifierPreferenceProjectRelative, + ImportModuleSpecifierEnding: endingPreference, + }, + modulespecifiers.ModuleSpecifierOptions{OverrideImportMode: swappedMode}, + false, + ) + if len(alternate.Specifiers) == 0 || strings.Contains(alternate.Specifiers[0].AsString(), "/node_modules/") { + return "" + } + specifier = alternate.Specifiers[0].AsString() + } + return specifier +} + func (r *EmitResolver) PrecalculateDeclarationEmitVisibility(file *ast.SourceFile) { r.checkerMu.Lock() defer r.checkerMu.Unlock() diff --git a/tsc/internal/checker/nodebuilderimpl.go b/tsc/internal/checker/nodebuilderimpl.go index 3d84ff3bd4094..598b13e2ea056 100644 --- a/tsc/internal/checker/nodebuilderimpl.go +++ b/tsc/internal/checker/nodebuilderimpl.go @@ -848,9 +848,18 @@ func (b *NodeBuilderImpl) createAccessFromSymbolChain(chain []*ast.Symbol, index func (b *NodeBuilderImpl) symbolToExpression(symbol *ast.Symbol, mask ast.SymbolFlags) *ast.Expression { b.ctx.tracker.TrackSymbol(symbol, b.ctx.enclosingDeclaration, mask) + b.recordRemappedSymbolReference(symbol) return b.symbolToExpressionWorker(symbol, mask) } +func (b *NodeBuilderImpl) recordRemappedSymbolReference(symbol *ast.Symbol) { + if remapper, ok := b.ctx.tracker.(nodebuilder.SymbolRemapper); ok { + if remapped := remapper.GetRemappedSymbol(symbol); remapped != nil { + b.ctx.remappedSymbolReferences[ast.GetSymbolId(symbol)] = remapped + } + } +} + func (b *NodeBuilderImpl) symbolToExpressionWorker(symbol *ast.Symbol, mask ast.SymbolFlags) *ast.Expression { chain := b.lookupSymbolChainWorker(symbol, mask, false) return b.createExpressionFromSymbolChain(chain, len(chain)-1) @@ -2450,6 +2459,14 @@ func (b *NodeBuilderImpl) trackComputedName(accessExpression *ast.Node, enclosin // get symbol of the first identifier of the entityName firstIdentifier := ast.GetFirstIdentifier(accessExpression) name := b.ch.resolveName(enclosingDeclaration, firstIdentifier.Text(), ast.SymbolFlagsValue|ast.SymbolFlagsExportValue, nil /*nameNotFoundMessage*/, true /*isUse*/, false) + sourceSymbol := b.ch.getSymbolOfNameOrPropertyAccessExpression(ast.GetReparsedNodeForNode(firstIdentifier)) + if sourceSymbol != nil && sourceSymbol != name && b.ch.getTypeOfSymbol(sourceSymbol).flags&TypeFlagsUniqueESSymbol != 0 { + // If the destination spelling resolves to a same-named local, track the + // symbol from the source declaration so declaration emit can import it + // under a collision-free local name. + b.ctx.tracker.TrackSymbol(sourceSymbol, enclosingDeclaration, ast.SymbolFlagsValue) + return + } if name != nil { b.ctx.tracker.TrackSymbol(name, enclosingDeclaration, ast.SymbolFlagsValue) } else { @@ -2586,7 +2603,9 @@ func (b *NodeBuilderImpl) getPropertyNameNodeForSymbolFromNameType(symbol *ast.S if nameType.flags&TypeFlagsUniqueESSymbol != 0 { // The reference was tracked in the destination scope by trackComputedName. // Reconstructing its spelling in the source scope must not paint that scope's declarations visible. - return b.f.NewComputedPropertyName(b.symbolToExpressionWorker(nameType.AsUniqueESSymbolType().symbol, ast.SymbolFlagsValue)) + symbol := nameType.AsUniqueESSymbolType().symbol + b.recordRemappedSymbolReference(symbol) + return b.f.NewComputedPropertyName(b.symbolToExpressionWorker(symbol, ast.SymbolFlagsValue)) } return nil } diff --git a/tsc/internal/checker/nodecopy.go b/tsc/internal/checker/nodecopy.go index 71ed761dcbdc8..261f18c611c82 100644 --- a/tsc/internal/checker/nodecopy.go +++ b/tsc/internal/checker/nodecopy.go @@ -186,6 +186,13 @@ func (w *wrappingTracker) TrackSymbol(symbol *ast.Symbol, enclosingDeclaration * return false } +func (w *wrappingTracker) GetRemappedSymbol(symbol *ast.Symbol) *ast.Symbol { + if remapper, ok := w.wrapped.(nodebuilder.SymbolRemapper); ok { + return remapper.GetRemappedSymbol(symbol) + } + return nil +} + func newWrappingTracker(inner nodebuilder.SymbolTracker, bound *recoveryBoundary) *wrappingTracker { return &wrappingTracker{ wrapped: inner, diff --git a/tsc/internal/checker/symboltracker.go b/tsc/internal/checker/symboltracker.go index 7ee61191feace..fe10b7050163e 100644 --- a/tsc/internal/checker/symboltracker.go +++ b/tsc/internal/checker/symboltracker.go @@ -39,6 +39,13 @@ func (this *SymbolTrackerImpl) TrackSymbol(symbol *ast.Symbol, enclosingDeclarat return false } +func (this *SymbolTrackerImpl) GetRemappedSymbol(symbol *ast.Symbol) *ast.Symbol { + if remapper, ok := this.inner.(nodebuilder.SymbolRemapper); ok { + return remapper.GetRemappedSymbol(symbol) + } + return nil +} + func (this *SymbolTrackerImpl) ReportInaccessibleThisError() { this.onDiagnosticReported() if this.inner == nil { diff --git a/tsc/internal/nodebuilder/types.go b/tsc/internal/nodebuilder/types.go index a3f6b928101ef..51038ebf932ab 100644 --- a/tsc/internal/nodebuilder/types.go +++ b/tsc/internal/nodebuilder/types.go @@ -22,6 +22,12 @@ type SymbolTracker interface { PopErrorFallbackNode() } +// SymbolRemapper optionally provides the name to use when a referenced symbol +// is represented by a generated binding during declaration emit. +type SymbolRemapper interface { + GetRemappedSymbol(symbol *ast.Symbol) *ast.Symbol +} + // NOTE: If modifying this enum, must modify `TypeFormatFlags` too! type Flags uint32 diff --git a/tsc/internal/printer/emitresolver.go b/tsc/internal/printer/emitresolver.go index d86a0ca378ecc..d6ac77a1038ef 100644 --- a/tsc/internal/printer/emitresolver.go +++ b/tsc/internal/printer/emitresolver.go @@ -97,6 +97,9 @@ type EmitResolver interface { // declaration emit checker functionality projections PrecalculateDeclarationEmitVisibility(file *ast.SourceFile) IsSymbolAccessible(symbol *ast.Symbol, enclosingDeclaration *ast.Node, meaning ast.SymbolFlags, shouldComputeAliasToMarkVisible bool) SymbolAccessibilityResult + IsUniqueESSymbol(symbol *ast.Symbol) bool + IsNameResolvableToSymbol(location *ast.Node, name string, target *ast.Symbol) bool + GetModuleSpecifierForSymbol(symbol *ast.Symbol, enclosingDeclaration *ast.Node) string IsEntityNameVisible(entityName *ast.Node, enclosingDeclaration *ast.Node) SymbolAccessibilityResult // previously SymbolVisibilityResult in strada - ErrorModuleName never set IsExpandoFunctionDeclaration(node *ast.Node) bool IsExpandoFunctionDeclarationUnsafe(node *ast.Node) bool diff --git a/tsc/internal/transformers/declarations/tracker.go b/tsc/internal/transformers/declarations/tracker.go index 14d0fcd5e5032..563d554c3771b 100644 --- a/tsc/internal/transformers/declarations/tracker.go +++ b/tsc/internal/transformers/declarations/tracker.go @@ -197,10 +197,22 @@ func (s *SymbolTrackerImpl) TrackSymbol(symbol *ast.Symbol, enclosingDeclaration s.classSymbolTracked = true return false } - issuedDiagnostic := s.handleSymbolAccessibilityError(s.resolver.IsSymbolAccessible(symbol, enclosingDeclaration, meaning /*shouldComputeAliasToMarkVisible*/, true)) + accessibility := s.resolver.IsSymbolAccessible(symbol, enclosingDeclaration, meaning /*shouldComputeAliasToMarkVisible*/, true) + if meaning&ast.SymbolFlagsValue != 0 && s.resolver.IsUniqueESSymbol(symbol) && + !s.resolver.IsNameResolvableToSymbol(enclosingDeclaration, symbol.Name(), symbol) && s.state.addUniqueSymbolImport != nil && s.state.addUniqueSymbolImport(symbol) { + return false + } + issuedDiagnostic := s.handleSymbolAccessibilityError(accessibility) return issuedDiagnostic } +func (s *SymbolTrackerImpl) GetRemappedSymbol(symbol *ast.Symbol) *ast.Symbol { + if s.state.remappedSymbols == nil { + return nil + } + return s.state.remappedSymbols[ast.GetSymbolId(symbol)] +} + func (s *SymbolTrackerImpl) handleSymbolAccessibilityError(symbolAccessibilityResult printer.SymbolAccessibilityResult) bool { if symbolAccessibilityResult.Accessibility == printer.SymbolAccessibilityAccessible { // Add aliases back onto the possible imports list if they're not there so we can try them again with updated visibility info @@ -246,6 +258,8 @@ type SymbolTrackerSharedState struct { currentSourceFile *ast.SourceFile resolver printer.EmitResolver reportExpandoFunctionErrors func(node *ast.Node) + addUniqueSymbolImport func(symbol *ast.Symbol) bool + remappedSymbols map[ast.SymbolId]*ast.Symbol } func (s *SymbolTrackerSharedState) addDiagnostic(diag *ast.Diagnostic) { diff --git a/tsc/internal/transformers/declarations/transform.go b/tsc/internal/transformers/declarations/transform.go index 7e362d9fb1660..0b3fbe37b5111 100644 --- a/tsc/internal/transformers/declarations/transform.go +++ b/tsc/internal/transformers/declarations/transform.go @@ -78,6 +78,9 @@ type DeclarationTransformer struct { expandoHosts map[ast.NodeId]*ast.Node // store the result of transforming expando hosts so they can be inserted later if the host is actually referenced expandoMembers map[ast.NodeId][]*ast.Node // store any found expando _members_ after transforming them so *if* the host is referenced, they can be emitted alongside it deferredExpandoAssignments map[ast.NodeId][]*ast.BinaryExpression // expando assignments whose host wasn't visible when collected, processed if the host is late-marked visible + generatedSymbolImports []*ast.Node + generatedSymbolImportIds map[ast.SymbolId]*ast.Symbol + generatedSymbolImportNames map[string]bool seenProperties collections.Set[thisPropertyAssignmentKey] thisPropertyAssignmentsCollected []*ast.Node rawReferencedFiles []ReferencedFilePair @@ -113,6 +116,7 @@ func NewDeclarationTransformer(host DeclarationEmitHost, resolver printer.EmitRe resolver: resolver, declarationFilePath: declarationFilePath, } + tx.state.addUniqueSymbolImport = tx.addUniqueSymbolImport tx.state.reportExpandoFunctionErrors = func(node *ast.Node) { if !tx.state.isolatedDeclarations { return @@ -290,6 +294,10 @@ func (tx *DeclarationTransformer) visitSourceFile(node *ast.SourceFile) *ast.Nod tx.resultHasExternalModuleIndicator = false tx.suppressNewDiagnosticContexts = false tx.state.lateMarkedStatements = make([]*ast.Node, 0) + tx.generatedSymbolImports = nil + tx.generatedSymbolImportIds = make(map[ast.SymbolId]*ast.Symbol) + tx.generatedSymbolImportNames = make(map[string]bool) + tx.state.remappedSymbols = make(map[ast.SymbolId]*ast.Symbol) tx.lateStatementReplacementMap = make(map[ast.NodeId]*ast.Node) tx.expandoHosts = make(map[ast.NodeId]*ast.Node) tx.expandoMembers = make(map[ast.NodeId][]*ast.Node) @@ -351,6 +359,10 @@ func (tx *DeclarationTransformer) transformSourceFile(node *ast.SourceFile) *ast var combinedStatements *ast.StatementList statements := tx.Visitor().VisitNodes(node.Statements) combinedStatements = tx.transformAndReplaceLatePaintedStatements(statements) + if len(tx.generatedSymbolImports) > 0 { + combinedStatements = tx.Factory().NewNodeList(append(tx.generatedSymbolImports, combinedStatements.Nodes...)) + tx.resultHasExternalModuleIndicator = true + } combinedStatements = tx.appendCjsExports(combinedStatements) combinedStatements.Loc = statements.Loc // setTextRange if ast.IsExternalOrCommonJSModule(node) { @@ -377,6 +389,60 @@ func (tx *DeclarationTransformer) transformSourceFile(node *ast.SourceFile) *ast return result.AsNode() } +func (tx *DeclarationTransformer) addUniqueSymbolImport(symbol *ast.Symbol) bool { + if symbol == nil { + return false + } + var exportedSymbol *ast.Symbol + for _, declaration := range symbol.Declarations() { + file := ast.GetSourceFileOfNode(declaration) + if file != nil && file.Symbol != nil { + if candidate := file.Symbol.Exports()[symbol.Name()]; candidate != nil { + if slices.Contains(candidate.Declarations(), declaration) { + exportedSymbol = candidate + break + } + } + } + } + if exportedSymbol == nil { + return false + } + symbolId := ast.GetSymbolId(symbol) + exportedSymbolId := ast.GetSymbolId(exportedSymbol) + if importedAlias := tx.generatedSymbolImportIds[exportedSymbolId]; importedAlias != nil { + tx.state.remappedSymbols[symbolId] = importedAlias + return true + } + specifier := tx.resolver.GetModuleSpecifierForSymbol(exportedSymbol, tx.enclosingDeclaration) + if specifier == "" { + return false + } + localName := exportedSymbol.Name() + for suffix := 1; tx.state.currentSourceFile.Locals[localName] != nil || tx.generatedSymbolImportNames[localName]; suffix++ { + localName = fmt.Sprintf("%s_%d", exportedSymbol.Name(), suffix) + } + importedName := tx.Factory().NewIdentifier(exportedSymbol.Name()) + localIdentifier := tx.Factory().NewIdentifier(localName) + var propertyName *ast.Node + if localName != exportedSymbol.Name() { + propertyName = importedName.AsNode() + } + importSpecifier := tx.Factory().NewImportSpecifier(false, propertyName, localIdentifier) + namedImports := tx.Factory().NewNamedImports(tx.Factory().NewNodeList([]*ast.Node{importSpecifier})) + importClause := tx.Factory().NewImportClause(ast.KindTypeKeyword, nil, namedImports) + moduleSpecifier := tx.Factory().NewStringLiteral(specifier, ast.TokenFlagsNone) + importDeclaration := tx.Factory().NewImportDeclaration(nil, importClause, moduleSpecifier, nil) + tx.generatedSymbolImports = append(tx.generatedSymbolImports, importDeclaration) + tx.generatedSymbolImportNames[localName] = true + alias := ast.NewSymbol() + alias.SetName(localName) + tx.generatedSymbolImportIds[exportedSymbolId] = alias + tx.state.remappedSymbols[symbolId] = alias + tx.state.remappedSymbols[exportedSymbolId] = alias + return true +} + func createEmptyExports(factory *ast.NodeFactory) *ast.Node { return factory.NewExportDeclaration(nil /*isTypeOnly*/, false, factory.NewNamedExports(factory.NewNodeList([]*ast.Node{})), nil, nil) } diff --git a/tsc/testdata/baselines/reference/compiler/declarationEmitReadonlyComputedProperty.js b/tsc/testdata/baselines/reference/compiler/declarationEmitReadonlyComputedProperty.js index 5bbd2be5deab6..36795e10fd692 100644 --- a/tsc/testdata/baselines/reference/compiler/declarationEmitReadonlyComputedProperty.js +++ b/tsc/testdata/baselines/reference/compiler/declarationEmitReadonlyComputedProperty.js @@ -45,3 +45,8 @@ export interface Interface { readonly [SYMBOL]: string; } export declare function createInstance(): Interface; +//// [index.d.ts] +import type { SYMBOL } from "./bug"; +export declare const spread: { + [SYMBOL]: string; +}; diff --git a/tsc/testdata/baselines/reference/compiler/uniqueSymbolDeclarationEmitImportsTag.js b/tsc/testdata/baselines/reference/compiler/uniqueSymbolDeclarationEmitImportsTag.js new file mode 100644 index 0000000000000..eb8a679a3858e --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/uniqueSymbolDeclarationEmitImportsTag.js @@ -0,0 +1,52 @@ +//// [tests/cases/compiler/uniqueSymbolDeclarationEmitImportsTag.ts] //// + +//// [brand.ts] +export declare const tag: unique symbol; +export type Branded = Type & { readonly [tag]: Name }; + +//// [otherBrand.ts] +export declare const tag: unique symbol; +export type OtherBranded = { readonly [tag]: "Other" }; + +//// [index.ts] +import type { Branded } from "./brand.js"; +import type { OtherBranded } from "./otherBrand.js"; + +declare const user: Branded<{ name: string }, "User"> & OtherBranded; +const tag = "local"; + +export const copy = { ...user }; + + +//// [brand.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +//// [otherBrand.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +//// [index.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.copy = void 0; +const tag = "local"; +exports.copy = { ...user }; + + +//// [brand.d.ts] +export declare const tag: unique symbol; +export type Branded = Type & { + readonly [tag]: Name; +}; +//// [otherBrand.d.ts] +export declare const tag: unique symbol; +export type OtherBranded = { + readonly [tag]: "Other"; +}; +//// [index.d.ts] +import type { tag as tag_1 } from "./brand.js"; +import type { tag as tag_2 } from "./otherBrand.js"; +export declare const copy: { + [tag_1]: "User"; + [tag_2]: "Other"; + name: string; +}; diff --git a/tsc/testdata/baselines/reference/compiler/uniqueSymbolDeclarationEmitImportsTag.symbols b/tsc/testdata/baselines/reference/compiler/uniqueSymbolDeclarationEmitImportsTag.symbols new file mode 100644 index 0000000000000..7dbca1b1caeae --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/uniqueSymbolDeclarationEmitImportsTag.symbols @@ -0,0 +1,44 @@ +//// [tests/cases/compiler/uniqueSymbolDeclarationEmitImportsTag.ts] //// + +=== /brand.ts === +export declare const tag: unique symbol; +>tag : Symbol(tag, Decl(brand.ts, 0, 20)) + +export type Branded = Type & { readonly [tag]: Name }; +>Branded : Symbol(Branded, Decl(brand.ts, 0, 40)) +>Type : Symbol(Type, Decl(brand.ts, 1, 20)) +>Name : Symbol(Name, Decl(brand.ts, 1, 25)) +>Type : Symbol(Type, Decl(brand.ts, 1, 20)) +>[tag] : Symbol([tag], Decl(brand.ts, 1, 57)) +>tag : Symbol(tag, Decl(brand.ts, 0, 20)) +>Name : Symbol(Name, Decl(brand.ts, 1, 25)) + +=== /otherBrand.ts === +export declare const tag: unique symbol; +>tag : Symbol(tag, Decl(otherBrand.ts, 0, 20)) + +export type OtherBranded = { readonly [tag]: "Other" }; +>OtherBranded : Symbol(OtherBranded, Decl(otherBrand.ts, 0, 40)) +>[tag] : Symbol([tag], Decl(otherBrand.ts, 1, 28)) +>tag : Symbol(tag, Decl(otherBrand.ts, 0, 20)) + +=== /index.ts === +import type { Branded } from "./brand.js"; +>Branded : Symbol(Branded, Decl(index.ts, 0, 13)) + +import type { OtherBranded } from "./otherBrand.js"; +>OtherBranded : Symbol(OtherBranded, Decl(index.ts, 1, 13)) + +declare const user: Branded<{ name: string }, "User"> & OtherBranded; +>user : Symbol(user, Decl(index.ts, 3, 13)) +>Branded : Symbol(Branded, Decl(index.ts, 0, 13)) +>name : Symbol(name, Decl(index.ts, 3, 29)) +>OtherBranded : Symbol(OtherBranded, Decl(index.ts, 1, 13)) + +const tag = "local"; +>tag : Symbol(tag, Decl(index.ts, 4, 5)) + +export const copy = { ...user }; +>copy : Symbol(copy, Decl(index.ts, 6, 12)) +>user : Symbol(user, Decl(index.ts, 3, 13)) + diff --git a/tsc/testdata/baselines/reference/compiler/uniqueSymbolDeclarationEmitImportsTag.types b/tsc/testdata/baselines/reference/compiler/uniqueSymbolDeclarationEmitImportsTag.types new file mode 100644 index 0000000000000..a15890dcb61ae --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/uniqueSymbolDeclarationEmitImportsTag.types @@ -0,0 +1,40 @@ +//// [tests/cases/compiler/uniqueSymbolDeclarationEmitImportsTag.ts] //// + +=== /brand.ts === +export declare const tag: unique symbol; +>tag : unique symbol + +export type Branded = Type & { readonly [tag]: Name }; +>Branded : Branded +>[tag] : Name +>tag : unique symbol + +=== /otherBrand.ts === +export declare const tag: unique symbol; +>tag : unique symbol + +export type OtherBranded = { readonly [tag]: "Other" }; +>OtherBranded : OtherBranded +>[tag] : "Other" +>tag : unique symbol + +=== /index.ts === +import type { Branded } from "./brand.js"; +>Branded : Branded + +import type { OtherBranded } from "./otherBrand.js"; +>OtherBranded : OtherBranded + +declare const user: Branded<{ name: string }, "User"> & OtherBranded; +>user : { name: string; } & { readonly [tag]: "User"; } & OtherBranded +>name : string + +const tag = "local"; +>tag : "local" +>"local" : "local" + +export const copy = { ...user }; +>copy : { [tag]: "User"; [tag]: "Other"; name: string; } +>{ ...user } : { [tag]: "User"; [tag]: "Other"; name: string; } +>user : { name: string; } & { readonly [tag]: "User"; } & OtherBranded + diff --git a/tsc/testdata/tests/cases/compiler/uniqueSymbolDeclarationEmitImportsTag.ts b/tsc/testdata/tests/cases/compiler/uniqueSymbolDeclarationEmitImportsTag.ts new file mode 100644 index 0000000000000..18cc3b69e315c --- /dev/null +++ b/tsc/testdata/tests/cases/compiler/uniqueSymbolDeclarationEmitImportsTag.ts @@ -0,0 +1,23 @@ +// @declaration: true +// @strict: true +// @module: nodenext +// @moduleResolution: nodenext + +// https://github.com/microsoft/TypeScript/issues/64812 + +// @filename: /brand.ts +export declare const tag: unique symbol; +export type Branded = Type & { readonly [tag]: Name }; + +// @filename: /otherBrand.ts +export declare const tag: unique symbol; +export type OtherBranded = { readonly [tag]: "Other" }; + +// @filename: /index.ts +import type { Branded } from "./brand.js"; +import type { OtherBranded } from "./otherBrand.js"; + +declare const user: Branded<{ name: string }, "User"> & OtherBranded; +const tag = "local"; + +export const copy = { ...user };