From 3aeb2c4a2744128a677b0655063a3001a2f29b12 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Tue, 13 Oct 2026 03:39:55 -0700 Subject: [PATCH 1/4] Check never-reducing intersection properties without creating them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getReducedType runs somePropertyReducesToNever on intersections. It used to create and cache a synthetic property for every property name shared by more than one constituent, only to inspect its flags. With large JSX prop intersections, as in MUI, most of those properties were never looked up again, yet they stayed in the intersection's property cache with their symbols and links. Collect the constituent properties without building the combined symbol, and only create the real property in the rare merged instantiation case. mui-docs: │ before │ after │ │ memory-used-B │ memory-used-B vs base │ MUIDocs 1.857Gi ± 0% 1.717Gi ± 0% -7.55% (p=0.002 n=6) │ before │ after │ │ allocs │ allocs vs base │ MUIDocs 27.29M ± 0% 25.19M ± 0% -7.72% (p=0.002 n=6) │ before │ after │ │ allocated-B │ allocated-B vs base │ MUIDocs 4.730Gi ± 0% 4.556Gi ± 0% -3.68% (p=0.002 n=6) │ before │ after │ │ symbols │ symbols vs base │ MUIDocs 4.332M ± 0% 3.663M ± 0% -15.43% (p=0.002 n=6) --- tsc/internal/checker/checker.go | 120 +++++++++++++++++++++++++++++--- 1 file changed, 110 insertions(+), 10 deletions(-) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index e58e96048e94b..e05dcbf9ca805 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -21916,15 +21916,34 @@ func (c *Checker) getUnionOrIntersectionProperty(t *Type, name string, skipObjec return prop } -func (c *Checker) createUnionOrIntersectionProperty(containingType *Type, name string, skipObjectFunctionPropertyAugment bool) *ast.Symbol { +// unionOrIntersectionPropertySources describes the constituent properties from +// which a union or intersection property would be synthesized. +type unionOrIntersectionPropertySources struct { + singleProp *ast.Symbol + propSet collections.OrderedSet[*ast.Symbol] + indexTypes []*Type + propFlags ast.SymbolFlags + optionalFlag ast.SymbolFlags + checkFlags ast.CheckFlags + syntheticFlag ast.CheckFlags + mergedInstantiations bool +} + +// isSynthetic reports whether the sources require a new synthetic symbol rather +// than reusing (or cloning) singleProp. +func (s *unionOrIntersectionPropertySources) isSynthetic() bool { + return s.propSet.Size() != 0 || s.checkFlags&ast.CheckFlagsReadPartial != 0 || len(s.indexTypes) != 0 +} + +func (c *Checker) getUnionOrIntersectionPropertySources(containingType *Type, name string, skipObjectFunctionPropertyAugment bool, sources *unionOrIntersectionPropertySources) bool { propFlags := ast.SymbolFlagsNone - var singleProp *ast.Symbol - var propSet collections.OrderedSet[*ast.Symbol] - var indexTypes []*Type isUnion := containingType.flags&TypeFlagsUnion != 0 // Flags we want to propagate to the result if they exist in all source symbols var checkFlags ast.CheckFlags var optionalFlag ast.SymbolFlags + var singleProp *ast.Symbol + propSet := &sources.propSet + var indexTypes []*Type if !isUnion { checkFlags = ast.CheckFlagsReadonly optionalFlag = ast.SymbolFlagsOptional @@ -22026,17 +22045,17 @@ func (c *Checker) createUnionOrIntersectionProperty(containingType *Type, name s } if singleProp == nil { // No property was found - return nil + return false } if isUnion && (propSet.Size() != 0 || checkFlags&ast.CheckFlagsPartial != 0) && checkFlags&(ast.CheckFlagsContainsPrivate|ast.CheckFlagsContainsProtected|ast.CheckFlagsContainsWritePrivate|ast.CheckFlagsContainsWriteProtected) != 0 && - !(propSet.Size() != 0 && c.hasCommonDeclaration(&propSet)) { + !(propSet.Size() != 0 && c.hasCommonDeclaration(propSet)) { // A property in a union has a private or protected declaration in one constituent, but is missing // or has a different declaration in another constituent. If the private or protected declaration is // for reading, we don't create a property. if checkFlags&(ast.CheckFlagsContainsPrivate|ast.CheckFlagsContainsProtected) != 0 { - return nil + return false } // Otherwise, if the private or protected declaration is for writing, reduce accessibility to that of // the most restricted constituent. @@ -22046,8 +22065,31 @@ func (c *Checker) createUnionOrIntersectionProperty(containingType *Type, name s checkFlags &^= ast.CheckFlagsContainsWritePublic } } - if propSet.Size() == 0 && checkFlags&ast.CheckFlagsReadPartial == 0 && len(indexTypes) == 0 { - if !mergedInstantiations { + sources.singleProp = singleProp + sources.indexTypes = indexTypes + sources.propFlags = propFlags + sources.optionalFlag = optionalFlag + sources.checkFlags = checkFlags + sources.syntheticFlag = syntheticFlag + sources.mergedInstantiations = mergedInstantiations + return true +} + +func (c *Checker) createUnionOrIntersectionProperty(containingType *Type, name string, skipObjectFunctionPropertyAugment bool) *ast.Symbol { + var sources unionOrIntersectionPropertySources + if !c.getUnionOrIntersectionPropertySources(containingType, name, skipObjectFunctionPropertyAugment, &sources) { + return nil + } + singleProp := sources.singleProp + propSet := &sources.propSet + indexTypes := sources.indexTypes + propFlags := sources.propFlags + optionalFlag := sources.optionalFlag + checkFlags := sources.checkFlags + syntheticFlag := sources.syntheticFlag + isUnion := containingType.flags&TypeFlagsUnion != 0 + if !sources.isSynthetic() { + if !sources.mergedInstantiations { return singleProp } // No symbol from a union/intersection should have a `.parent` set (since unions/intersections don't act as symbol parents) @@ -22356,7 +22398,7 @@ func (c *Checker) somePropertyReducesToNever(t *Type) bool { // Go in the order the properties were found so the combined properties are created in the same order every time. for propName, count := range counts.Entries() { if count > 1 { - if prop := c.getPropertyOfUnionOrIntersectionType(t, propName, true /*skipObjectFunctionPropertyAugment*/); prop != nil && c.isNeverReducedProperty(prop) { + if c.intersectionPropertyReducesToNever(t, propName) { return true } } @@ -22364,6 +22406,64 @@ func (c *Checker) somePropertyReducesToNever(t *Type) bool { return false } +// intersectionPropertyReducesToNever reports whether the combined property named +// name would satisfy isNeverReducedProperty, without creating and caching that +// property. Most such properties are never otherwise looked up, so creating +// them only to inspect their flags retains a large number of symbols. +func (c *Checker) intersectionPropertyReducesToNever(t *Type, name string) bool { + if prop := t.AsUnionOrIntersectionType().propertyCacheWithoutFunctionPropertyAugment[name]; prop != nil { + return c.isNeverReducedProperty(prop) + } + var sources unionOrIntersectionPropertySources + if !c.getUnionOrIntersectionPropertySources(t, name, true /*skipObjectFunctionPropertyAugment*/, &sources) { + return false + } + if !sources.isSynthetic() { + if !sources.mergedInstantiations { + return c.isNeverReducedProperty(sources.singleProp) + } + prop := c.getUnionOrIntersectionProperty(t, name, true /*skipObjectFunctionPropertyAugment*/) + return prop != nil && c.isNeverReducedProperty(prop) + } + if sources.propSet.Size() == 0 { + sources.propSet.Add(sources.singleProp) + } + checkFlags := sources.checkFlags + var firstType *Type + var firstValueDeclaration *ast.Node + hasNonUniformValueDeclaration := false + propTypes := make([]*Type, 0, sources.propSet.Size()+len(sources.indexTypes)) + for prop := range sources.propSet.Values() { + if firstValueDeclaration == nil { + firstValueDeclaration = prop.ValueDeclaration() + } else if prop.ValueDeclaration() != nil && prop.ValueDeclaration() != firstValueDeclaration { + hasNonUniformValueDeclaration = true + } + propType := c.getTypeOfSymbol(prop) + if firstType == nil { + firstType = propType + } + if propType != firstType { + checkFlags |= ast.CheckFlagsHasNonUniformType + } + if isLiteralType(propType) || c.isPatternLiteralType(propType) { + checkFlags |= ast.CheckFlagsHasLiteralType + } + if propType.flags&TypeFlagsNever != 0 && propType != c.uniqueLiteralType { + checkFlags |= ast.CheckFlagsHasNeverType + } + propTypes = append(propTypes, propType) + } + propTypes = append(propTypes, sources.indexTypes...) + hasValueDeclaration := !hasNonUniformValueDeclaration && firstValueDeclaration != nil + if !hasValueDeclaration && checkFlags&ast.CheckFlagsContainsPrivate != 0 { + return true + } + return (sources.propFlags|sources.optionalFlag)&ast.SymbolFlagsOptional == 0 && + checkFlags&(ast.CheckFlagsNonUniformAndLiteral|ast.CheckFlagsHasNeverType) == ast.CheckFlagsNonUniformAndLiteral && + c.getIntersectionType(propTypes).flags&TypeFlagsNever != 0 +} + func (c *Checker) getReducedUnionType(unionType *Type) *Type { reducedTypes := core.SameMap(unionType.Types(), c.getReducedType) if core.Same(reducedTypes, unionType.Types()) { From c0a419cc93702d0ee5d14fe093e37c1384dd0210 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Tue, 13 Oct 2026 04:17:24 -0700 Subject: [PATCH 2/4] Avoid map-backed sets for union and intersection property sources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Synthesizing or probing a union or intersection property gathers the distinct constituent properties into a set. That set rarely holds more than a handful of symbols, but collections.OrderedSet always allocates both a map and a key slice, and this runs for every property of every combined type that is enumerated during relation checks. Collect them in the checker's orderedSet instead, which scans a slice for small sets and only builds a map once the set grows large enough for hashing to pay off. mui-docs: │ before │ after │ │ allocs │ allocs vs base │ MUIDocs 25.19M ± 0% 23.32M ± 1% -7.41% (p=0.002 n=6) │ before │ after │ │ allocated-B │ allocated-B vs base │ MUIDocs 4.556Gi ± 0% 4.389Gi ± 1% -3.66% (p=0.002 n=6) --- tsc/internal/checker/checker.go | 44 +++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index e05dcbf9ca805..fcad8631a55f0 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -21919,8 +21919,10 @@ func (c *Checker) getUnionOrIntersectionProperty(t *Type, name string, skipObjec // unionOrIntersectionPropertySources describes the constituent properties from // which a union or intersection property would be synthesized. type unionOrIntersectionPropertySources struct { - singleProp *ast.Symbol - propSet collections.OrderedSet[*ast.Symbol] + singleProp *ast.Symbol + // props holds distinct constituent properties in first-seen order, or is + // empty when every constituent shares singleProp. + props orderedSet[*ast.Symbol] indexTypes []*Type propFlags ast.SymbolFlags optionalFlag ast.SymbolFlags @@ -21932,7 +21934,13 @@ type unionOrIntersectionPropertySources struct { // isSynthetic reports whether the sources require a new synthetic symbol rather // than reusing (or cloning) singleProp. func (s *unionOrIntersectionPropertySources) isSynthetic() bool { - return s.propSet.Size() != 0 || s.checkFlags&ast.CheckFlagsReadPartial != 0 || len(s.indexTypes) != 0 + return len(s.props.values) != 0 || s.checkFlags&ast.CheckFlagsReadPartial != 0 || len(s.indexTypes) != 0 +} + +func (s *unionOrIntersectionPropertySources) addProp(prop *ast.Symbol) { + if !s.props.contains(prop) { + s.props.add(prop) + } } func (c *Checker) getUnionOrIntersectionPropertySources(containingType *Type, name string, skipObjectFunctionPropertyAugment bool, sources *unionOrIntersectionPropertySources) bool { @@ -21942,7 +21950,6 @@ func (c *Checker) getUnionOrIntersectionPropertySources(containingType *Type, na var checkFlags ast.CheckFlags var optionalFlag ast.SymbolFlags var singleProp *ast.Symbol - propSet := &sources.propSet var indexTypes []*Type if !isUnion { checkFlags = ast.CheckFlagsReadonly @@ -21979,10 +21986,10 @@ func (c *Checker) getUnionOrIntersectionPropertySources(containingType *Type, na // back and not `Array.length` when we're looking at a `.length` access on a `string[] | number[]` mergedInstantiations = singleProp.Parent() != nil && len(c.getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(singleProp.Parent())) != 0 } else { - if propSet.Size() == 0 { - propSet.Add(singleProp) + if len(sources.props.values) == 0 { + sources.addProp(singleProp) } - propSet.Add(prop) + sources.addProp(prop) } // classes created by mixins are represented as intersections // and overriding a property in a derived class redefines it completely at runtime @@ -22048,9 +22055,9 @@ func (c *Checker) getUnionOrIntersectionPropertySources(containingType *Type, na return false } if isUnion && - (propSet.Size() != 0 || checkFlags&ast.CheckFlagsPartial != 0) && + (len(sources.props.values) != 0 || checkFlags&ast.CheckFlagsPartial != 0) && checkFlags&(ast.CheckFlagsContainsPrivate|ast.CheckFlagsContainsProtected|ast.CheckFlagsContainsWritePrivate|ast.CheckFlagsContainsWriteProtected) != 0 && - !(propSet.Size() != 0 && c.hasCommonDeclaration(propSet)) { + !(len(sources.props.values) != 0 && c.hasCommonDeclaration(sources.props.values)) { // A property in a union has a private or protected declaration in one constituent, but is missing // or has a different declaration in another constituent. If the private or protected declaration is // for reading, we don't create a property. @@ -22081,7 +22088,6 @@ func (c *Checker) createUnionOrIntersectionProperty(containingType *Type, name s return nil } singleProp := sources.singleProp - propSet := &sources.propSet indexTypes := sources.indexTypes propFlags := sources.propFlags optionalFlag := sources.optionalFlag @@ -22112,8 +22118,8 @@ func (c *Checker) createUnionOrIntersectionProperty(containingType *Type, name s links.setWriteType(c, c.getWriteTypeOfSymbol(singleProp)) return clone } - if propSet.Size() == 0 { - propSet.Add(singleProp) + if len(sources.props.values) == 0 { + sources.props.add(singleProp) } var declarations []*ast.Node var firstType *Type @@ -22122,7 +22128,7 @@ func (c *Checker) createUnionOrIntersectionProperty(containingType *Type, name s var writeTypes []*Type var firstValueDeclaration *ast.Node var hasNonUniformValueDeclaration bool - for prop := range propSet.Values() { + for _, prop := range sources.props.values { if firstValueDeclaration == nil { firstValueDeclaration = prop.ValueDeclaration() } else if prop.ValueDeclaration() != nil && prop.ValueDeclaration() != firstValueDeclaration { @@ -22206,9 +22212,9 @@ func isPrototypeProperty(symbol *ast.Symbol) bool { return symbol.Flags()&ast.SymbolFlagsMethod != 0 || symbol.CheckFlags()&ast.CheckFlagsSyntheticMethod != 0 } -func (c *Checker) hasCommonDeclaration(symbols *collections.OrderedSet[*ast.Symbol]) bool { +func (c *Checker) hasCommonDeclaration(symbols []*ast.Symbol) bool { var commonDeclarations collections.Set[*ast.Node] - for symbol := range symbols.Values() { + for _, symbol := range symbols { if len(symbol.Declarations()) == 0 { return false } @@ -22425,15 +22431,15 @@ func (c *Checker) intersectionPropertyReducesToNever(t *Type, name string) bool prop := c.getUnionOrIntersectionProperty(t, name, true /*skipObjectFunctionPropertyAugment*/) return prop != nil && c.isNeverReducedProperty(prop) } - if sources.propSet.Size() == 0 { - sources.propSet.Add(sources.singleProp) + if len(sources.props.values) == 0 { + sources.props.add(sources.singleProp) } checkFlags := sources.checkFlags var firstType *Type var firstValueDeclaration *ast.Node hasNonUniformValueDeclaration := false - propTypes := make([]*Type, 0, sources.propSet.Size()+len(sources.indexTypes)) - for prop := range sources.propSet.Values() { + propTypes := make([]*Type, 0, len(sources.props.values)+len(sources.indexTypes)) + for _, prop := range sources.props.values { if firstValueDeclaration == nil { firstValueDeclaration = prop.ValueDeclaration() } else if prop.ValueDeclaration() != nil && prop.ValueDeclaration() != firstValueDeclaration { From e2fcf0f966dfb3b27130dd77fc3acc2af96160a8 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Tue, 13 Oct 2026 08:20:26 -0700 Subject: [PATCH 3/4] Reuse name sets when enumerating combined type properties MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enumerating the properties of a union or intersection tracks which names have already been combined. A fresh set is allocated for every enumeration, and on large projects those sets account for a sizable share of all allocated bytes, adding GC work for no lasting benefit. Borrow the set from a per-checker free list instead. Enumeration can recurse into other combined types, so each call still takes its own set. mui-docs: │ before │ after │ │ allocated-B │ allocated-B vs base │ MUIDocs 4.389Gi ± 1% 4.092Gi ± 0% -6.76% (p=0.002 n=6) │ before │ after │ │ allocs │ allocs vs base │ MUIDocs 23.32M ± 1% 23.18M ± 0% -0.60% (p=0.002 n=6) --- tsc/internal/checker/checker.go | 7 ++++--- tsc/internal/checker/utilities.go | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index fcad8631a55f0..6a08780ca9584 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -692,6 +692,7 @@ type Checker struct { structuredTypeExtraArena core.Arena[structuredTypeExtra] freeTypeSets *temporaryTypeSet freePropertyCounts *temporaryPropertyCounts + freeNameSets []*collections.Set[string] mergedSymbols map[*ast.Symbol]*ast.Symbol mergedExportsChecked collections.Set[*ast.Symbol] factory ast.NodeFactory @@ -19323,12 +19324,12 @@ func (c *Checker) getPropertiesOfObjectType(t *Type) []*ast.Symbol { func (c *Checker) getPropertiesOfUnionOrIntersectionType(t *Type) []*ast.Symbol { d := t.AsUnionOrIntersectionType() if d.resolvedProperties == nil { - var checked collections.Set[string] + checked := c.getTemporaryNameSet() + defer c.releaseTemporaryNameSet(checked) props := []*ast.Symbol{} for _, current := range d.types { for _, prop := range c.getPropertiesOfType(current) { - if !checked.Has(prop.Name()) { - checked.Add(prop.Name()) + if checked.AddIfAbsent(prop.Name()) { combinedProp := c.getPropertyOfUnionOrIntersectionType(t, prop.Name(), t.flags&TypeFlagsIntersection != 0 /*skipObjectFunctionPropertyAugment*/) if combinedProp != nil { props = append(props, combinedProp) diff --git a/tsc/internal/checker/utilities.go b/tsc/internal/checker/utilities.go index 517ffcba86cc2..cf60b064a1855 100644 --- a/tsc/internal/checker/utilities.go +++ b/tsc/internal/checker/utilities.go @@ -957,6 +957,26 @@ func (c *Checker) releaseTemporaryPropertyCounts(counts *temporaryPropertyCounts c.freePropertyCounts = counts } +// Property enumeration of a combined type may recursively enumerate other +// combined types, so each call borrows a separate name set. +func (c *Checker) getTemporaryNameSet() *collections.Set[string] { + if n := len(c.freeNameSets); n != 0 { + set := c.freeNameSets[n-1] + c.freeNameSets = c.freeNameSets[:n-1] + return set + } + return &collections.Set[string]{} +} + +func (c *Checker) releaseTemporaryNameSet(set *collections.Set[string]) { + // Do not retain unusually large tables in the free list. + if set.Len() > 4096 { + return + } + set.Clear() + c.freeNameSets = append(c.freeNameSets, set) +} + func (s *orderedSet[T]) contains(value T) bool { if s.valuesByKey == nil { return slices.Contains(s.values, value) From 5a40773ecd87d8c47e0100edba74d5de3079ad63 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 14 Oct 2026 10:13:16 -0700 Subject: [PATCH 4/4] Drop popped name sets from the free list's backing array Reslicing the free list left the popped set reachable from the backing array, so a set that later grew past the retention limit and was discarded on release stayed alive until its slot was overwritten. --- tsc/internal/checker/utilities.go | 1 + 1 file changed, 1 insertion(+) diff --git a/tsc/internal/checker/utilities.go b/tsc/internal/checker/utilities.go index cf60b064a1855..a3d189dd804ab 100644 --- a/tsc/internal/checker/utilities.go +++ b/tsc/internal/checker/utilities.go @@ -962,6 +962,7 @@ func (c *Checker) releaseTemporaryPropertyCounts(counts *temporaryPropertyCounts func (c *Checker) getTemporaryNameSet() *collections.Set[string] { if n := len(c.freeNameSets); n != 0 { set := c.freeNameSets[n-1] + c.freeNameSets[n-1] = nil c.freeNameSets = c.freeNameSets[:n-1] return set }