From 6677934a039ca00b2989833442b64a5f16f8c1a6 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 29 Oct 2026 14:58:08 -0700 Subject: [PATCH 1/5] Add fast exits to getApparentType and getReducedType Both functions are called on nearly every property, signature, and index lookup, and the overwhelmingly common input is a plain object type, for which both return the type unchanged. They were only able to reach that answer after a chain of flag tests (and, for getApparentType, a non-inlinable call), and getReducedApparentType paid for it three times. Split each into a small inlinable wrapper that returns early for types that cannot change (non-mapped object types for the apparent type, anything other than a union or intersection for reduction), and let getReducedApparentType skip all three steps for non-mapped object types. Co-authored-by: Steven Chong <25894545+teamchong@users.noreply.github.com> --- tsc/internal/checker/checker.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index d9cb44e00a903..fba720d672610 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -22331,6 +22331,15 @@ func (c *Checker) isMappedTypeGenericIndexedAccess(t *Type) bool { * type itself. */ func (c *Checker) getApparentType(t *Type) *Type { + // Object types other than mapped types are their own apparent types. Checking this here + // keeps the common case cheap enough to inline. + if t.flags&TypeFlagsObject != 0 && t.objectFlags&ObjectFlagsMapped == 0 { + return t + } + return c.getApparentTypeWorker(t) +} + +func (c *Checker) getApparentTypeWorker(t *Type) *Type { originalType := t if t.flags&TypeFlagsInstantiable != 0 { t = c.getBaseConstraintOfType(t) @@ -22421,6 +22430,15 @@ func (c *Checker) getApparentTypeOfIntersectionType(t *Type, thisArgument *Type) * no constituent property has type 'never', but the intersection of the constituent property types is 'never'. */ func (c *Checker) getReducedType(t *Type) *Type { + // Only unions and intersections can reduce. Checking this here keeps the common case + // cheap enough to inline. + if t.flags&(TypeFlagsUnion|TypeFlagsIntersection) == 0 { + return t + } + return c.getReducedTypeWorker(t) +} + +func (c *Checker) getReducedTypeWorker(t *Type) *Type { switch { case t.flags&TypeFlagsUnion != 0: if t.objectFlags&ObjectFlagsContainsIntersections != 0 { @@ -22507,6 +22525,10 @@ func (c *Checker) getReducedApparentType(t *Type) *Type { // type reduction both before and after obtaining the apparent type. For example, given a type parameter // 'T extends A | B', the type 'T & X' becomes 'A & X | B & X' after obtaining the apparent type, and // that type may need further reduction to remove empty intersections. + if t.flags&TypeFlagsObject != 0 && t.objectFlags&ObjectFlagsMapped == 0 { + // Neither reduces nor has a different apparent type. + return t + } return c.getReducedType(c.getApparentType(c.getReducedType(t))) } From 962cc50be2569e38fa5297aa4326c453391fdee9 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 29 Oct 2026 14:59:17 -0700 Subject: [PATCH 2/5] Look up applicable index infos without building slices getApplicableIndexInfoForName runs for every property name that is missing from a union constituent, and most types have no index signatures at all. It still created (and interned) a string literal type for the name before discovering there was nothing to match. Check for index infos first. findApplicableIndexInfo also allocated an 8-element slice on every call to collect applicable infos, although at most one is applicable in almost all cases. Track the first applicable info directly and only collect value types once a second one is found. Co-authored-by: Steven Chong <25894545+teamchong@users.noreply.github.com> --- tsc/internal/checker/checker.go | 51 +++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index fba720d672610..34d05115d73c4 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -19497,45 +19497,54 @@ func (c *Checker) getApplicableIndexInfo(t *Type, keyType *Type) *IndexInfo { } func (c *Checker) getApplicableIndexInfoForName(t *Type, name string) *IndexInfo { + indexInfos := c.getIndexInfosOfType(t) + // Most types have no index signatures, so avoid creating a literal type for the name. + if len(indexInfos) == 0 { + return nil + } if isLateBoundName(name) { - return c.getApplicableIndexInfo(t, c.esSymbolType) + return c.findApplicableIndexInfo(indexInfos, c.esSymbolType) } - return c.getApplicableIndexInfo(t, c.getStringLiteralType(name)) + return c.findApplicableIndexInfo(indexInfos, c.getStringLiteralType(name)) } func (c *Checker) findApplicableIndexInfo(indexInfos []*IndexInfo, keyType *Type) *IndexInfo { // Index signatures for type 'string' are considered only when no other index signatures apply. var stringIndexInfo *IndexInfo - applicableInfos := make([]*IndexInfo, 0, 8) + var applicableInfo *IndexInfo + // Value types of all applicable index signatures, collected only when there is more than one. + var applicableTypes []*Type + isReadonly := true for _, info := range indexInfos { if info.keyType == c.stringType { stringIndexInfo = info } else if c.isApplicableIndexType(keyType, info.keyType) { - applicableInfos = append(applicableInfos, info) + if applicableInfo == nil { + applicableInfo = info + } else { + if applicableTypes == nil { + applicableTypes = append(make([]*Type, 0, len(indexInfos)), applicableInfo.valueType) + } + applicableTypes = append(applicableTypes, info.valueType) + } + if !info.isReadonly { + isReadonly = false + } } } - // When more than one index signature is applicable we create a synthetic IndexInfo. Instead of computing - // the intersected key type, we just use unknownType for the key type as nothing actually depends on the - // keyType property of the returned IndexInfo. - switch len(applicableInfos) { - case 0: + if applicableInfo == nil { if stringIndexInfo != nil && c.isApplicableIndexType(keyType, c.stringType) { return stringIndexInfo } return nil - case 1: - return applicableInfos[0] - default: - isReadonly := true - types := make([]*Type, len(applicableInfos)) - for i, info := range applicableInfos { - types[i] = info.valueType - if !info.isReadonly { - isReadonly = false - } - } - return c.newIndexInfo(c.unknownType, c.getIntersectionType(types), isReadonly, nil, nil) } + if applicableTypes == nil { + return applicableInfo + } + // When more than one index signature is applicable we create a synthetic IndexInfo. Instead of computing + // the intersected key type, we just use unknownType for the key type as nothing actually depends on the + // keyType property of the returned IndexInfo. + return c.newIndexInfo(c.unknownType, c.getIntersectionType(applicableTypes), isReadonly, nil, nil) } func (c *Checker) isApplicableIndexType(source *Type, target *Type) bool { From 6f27cdf9c72e2731b4b115463cae5144e1c97ef4 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 29 Oct 2026 15:00:41 -0700 Subject: [PATCH 3/5] Probe resolved object constituents directly for union properties Synthesizing a union or intersection property looks the name up in every constituent. Constituents are most often object types whose members are already resolved, where the lookup amounts to a single member table probe, yet each one went through getApparentType and the general getPropertyOfTypeEx path (reduced apparent type, member resolution, module and value checks) first. For such constituents, read the member directly when it is a plain value, or when it is missing and Object/Function augmentation is not requested. All other cases (module types, aliases, missing members that may come from the global Object or Function types) still take the full lookup, so the resulting property is unchanged. Co-authored-by: Steven Chong <25894545+teamchong@users.noreply.github.com> --- tsc/internal/checker/checker.go | 189 ++++++++++++++++++-------------- 1 file changed, 106 insertions(+), 83 deletions(-) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 34d05115d73c4..df57fafaad277 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -19431,6 +19431,22 @@ func (c *Checker) getPropertyOfTypeEx(t *Type, name string, skipObjectFunctionPr return nil } +// Looks up a property of an object type whose members are already resolved, giving the same result as +// getPropertyOfTypeEx on its apparent type without the work of getting there. Reports false when the +// full lookup is needed: for other kinds of types, module types, members that aren't plain values, and +// missing members that may come from the global Object or Function types. +func getValueMemberOfResolvedObjectType(t *Type, name string, skipObjectFunctionPropertyAugment bool) (*ast.Symbol, bool) { + if t.flags != TypeFlagsObject || t.objectFlags&(ObjectFlagsMapped|ObjectFlagsMembersResolved) != ObjectFlagsMembersResolved || + t.symbol != nil && t.symbol.Flags()&ast.SymbolFlagsValueModule != 0 { + return nil, false + } + prop := t.AsStructuredType().getMember(name) + if prop == nil { + return nil, skipObjectFunctionPropertyAugment + } + return prop, prop.Flags()&ast.SymbolFlagsValue != 0 +} + // Return the type of the given property in the given type, or nil if no such property exists func (c *Checker) getTypeOfPropertyOfType(t *Type, name string) *Type { prop := c.getPropertyOfType(t, name) @@ -22057,95 +22073,102 @@ func (c *Checker) createUnionOrIntersectionProperty(containingType *Type, name s syntheticFlag := ast.CheckFlagsSyntheticMethod mergedInstantiations := false for _, current := range containingType.Types() { - t := c.getApparentType(current) - if !c.isErrorType(t) && t.flags&TypeFlagsNever == 0 { - prop := c.getPropertyOfTypeEx(t, name, skipObjectFunctionPropertyAugment, false) - var modifiers ast.ModifierFlags - if prop != nil { - modifiers = getDeclarationModifierFlagsFromSymbol(prop) - writeModifiers := getDeclarationModifierFlagsFromSymbolEx(prop, true /*isWrite*/) - if prop.Flags()&ast.SymbolFlagsClassMember != 0 { - if isUnion { - optionalFlag |= prop.Flags() & ast.SymbolFlagsOptional - } else { - optionalFlag &= prop.Flags() - } - } - if singleProp == nil { - singleProp = prop - propFlags = core.OrElse(prop.Flags()&ast.SymbolFlagsAccessor, ast.SymbolFlagsProperty) - } else if prop != singleProp { - isInstantiation := c.getTargetSymbol(prop) == c.getTargetSymbol(singleProp) - // If the symbols are instances of one another with identical types - consider the symbols - // equivalent and just use the first one, which thus allows us to avoid eliding private - // members when intersecting a (this-)instantiations of a class with its raw base or another instance - if isInstantiation && c.compareProperties(singleProp, prop, compareTypesEqual) == TernaryTrue { - // If we merged instantiations of a generic type, we replicate the symbol parent resetting behavior we used - // to do when we recorded multiple distinct symbols so that we still get, eg, `Array.length` printed - // 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) - } - propSet.Add(prop) - } - // classes created by mixins are represented as intersections - // and overriding a property in a derived class redefines it completely at runtime - // so a get accessor can't be merged with a set accessor in a base class, - // for that reason the accessor flags are only used when they are the same in all constituents - if propFlags&ast.SymbolFlagsAccessor != 0 && (prop.Flags()&ast.SymbolFlagsAccessor != (propFlags & ast.SymbolFlagsAccessor)) { - propFlags = (propFlags &^ ast.SymbolFlagsAccessor) | ast.SymbolFlagsProperty + t := current + var prop *ast.Symbol + if member, ok := getValueMemberOfResolvedObjectType(current, name, skipObjectFunctionPropertyAugment); ok { + prop = member + } else { + t = c.getApparentType(current) + if c.isErrorType(t) || t.flags&TypeFlagsNever != 0 { + continue + } + prop = c.getPropertyOfTypeEx(t, name, skipObjectFunctionPropertyAugment, false) + } + var modifiers ast.ModifierFlags + if prop != nil { + modifiers = getDeclarationModifierFlagsFromSymbol(prop) + writeModifiers := getDeclarationModifierFlagsFromSymbolEx(prop, true /*isWrite*/) + if prop.Flags()&ast.SymbolFlagsClassMember != 0 { + if isUnion { + optionalFlag |= prop.Flags() & ast.SymbolFlagsOptional + } else { + optionalFlag &= prop.Flags() + } + } + if singleProp == nil { + singleProp = prop + propFlags = core.OrElse(prop.Flags()&ast.SymbolFlagsAccessor, ast.SymbolFlagsProperty) + } else if prop != singleProp { + isInstantiation := c.getTargetSymbol(prop) == c.getTargetSymbol(singleProp) + // If the symbols are instances of one another with identical types - consider the symbols + // equivalent and just use the first one, which thus allows us to avoid eliding private + // members when intersecting a (this-)instantiations of a class with its raw base or another instance + if isInstantiation && c.compareProperties(singleProp, prop, compareTypesEqual) == TernaryTrue { + // If we merged instantiations of a generic type, we replicate the symbol parent resetting behavior we used + // to do when we recorded multiple distinct symbols so that we still get, eg, `Array.length` printed + // 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) } + propSet.Add(prop) } - if isUnion && c.isReadonlySymbol(prop) { - checkFlags |= ast.CheckFlagsReadonly - } else if !isUnion && !c.isReadonlySymbol(prop) { - checkFlags &^= ast.CheckFlagsReadonly + // classes created by mixins are represented as intersections + // and overriding a property in a derived class redefines it completely at runtime + // so a get accessor can't be merged with a set accessor in a base class, + // for that reason the accessor flags are only used when they are the same in all constituents + if propFlags&ast.SymbolFlagsAccessor != 0 && (prop.Flags()&ast.SymbolFlagsAccessor != (propFlags & ast.SymbolFlagsAccessor)) { + propFlags = (propFlags &^ ast.SymbolFlagsAccessor) | ast.SymbolFlagsProperty } - if modifiers&ast.ModifierFlagsProtected != 0 && modifiers&ast.ModifierFlagsPublic == 0 { - checkFlags |= ast.CheckFlagsContainsProtected - } else if modifiers&ast.ModifierFlagsPrivate != 0 && modifiers&ast.ModifierFlagsPublic == 0 { - checkFlags |= ast.CheckFlagsContainsPrivate - } else { - checkFlags |= ast.CheckFlagsContainsPublic - } - if writeModifiers&ast.ModifierFlagsProtected != 0 && writeModifiers&ast.ModifierFlagsPublic == 0 { - checkFlags |= ast.CheckFlagsContainsWriteProtected - } else if writeModifiers&ast.ModifierFlagsPrivate != 0 && writeModifiers&ast.ModifierFlagsPublic == 0 { - checkFlags |= ast.CheckFlagsContainsWritePrivate - } else { - checkFlags |= ast.CheckFlagsContainsWritePublic - } - if modifiers&ast.ModifierFlagsStatic != 0 { - checkFlags |= ast.CheckFlagsContainsStatic - } - if !isPrototypeProperty(prop) { - syntheticFlag = ast.CheckFlagsSyntheticProperty - } - } else if isUnion { - var indexInfo *IndexInfo - if !isLateBoundName(name) { - indexInfo = c.getApplicableIndexInfoForName(t, name) - } - if indexInfo != nil { - propFlags = propFlags&^ast.SymbolFlagsAccessor | ast.SymbolFlagsProperty - checkFlags |= ast.CheckFlagsWritePartial | core.IfElse(indexInfo.isReadonly, ast.CheckFlagsReadonly, 0) - if isTupleType(t) { - indexType := c.getRestTypeOfTupleType(t) - if indexType == nil { - indexType = c.undefinedType - } - indexTypes = append(indexTypes, indexType) - } else { - indexTypes = append(indexTypes, indexInfo.valueType) + } + if isUnion && c.isReadonlySymbol(prop) { + checkFlags |= ast.CheckFlagsReadonly + } else if !isUnion && !c.isReadonlySymbol(prop) { + checkFlags &^= ast.CheckFlagsReadonly + } + if modifiers&ast.ModifierFlagsProtected != 0 && modifiers&ast.ModifierFlagsPublic == 0 { + checkFlags |= ast.CheckFlagsContainsProtected + } else if modifiers&ast.ModifierFlagsPrivate != 0 && modifiers&ast.ModifierFlagsPublic == 0 { + checkFlags |= ast.CheckFlagsContainsPrivate + } else { + checkFlags |= ast.CheckFlagsContainsPublic + } + if writeModifiers&ast.ModifierFlagsProtected != 0 && writeModifiers&ast.ModifierFlagsPublic == 0 { + checkFlags |= ast.CheckFlagsContainsWriteProtected + } else if writeModifiers&ast.ModifierFlagsPrivate != 0 && writeModifiers&ast.ModifierFlagsPublic == 0 { + checkFlags |= ast.CheckFlagsContainsWritePrivate + } else { + checkFlags |= ast.CheckFlagsContainsWritePublic + } + if modifiers&ast.ModifierFlagsStatic != 0 { + checkFlags |= ast.CheckFlagsContainsStatic + } + if !isPrototypeProperty(prop) { + syntheticFlag = ast.CheckFlagsSyntheticProperty + } + } else if isUnion { + var indexInfo *IndexInfo + if !isLateBoundName(name) { + indexInfo = c.getApplicableIndexInfoForName(t, name) + } + if indexInfo != nil { + propFlags = propFlags&^ast.SymbolFlagsAccessor | ast.SymbolFlagsProperty + checkFlags |= ast.CheckFlagsWritePartial | core.IfElse(indexInfo.isReadonly, ast.CheckFlagsReadonly, 0) + if isTupleType(t) { + indexType := c.getRestTypeOfTupleType(t) + if indexType == nil { + indexType = c.undefinedType } - } else if isObjectLiteralType(t) && t.objectFlags&ObjectFlagsContainsSpread == 0 { - checkFlags |= ast.CheckFlagsWritePartial - indexTypes = append(indexTypes, c.undefinedType) + indexTypes = append(indexTypes, indexType) } else { - checkFlags |= ast.CheckFlagsReadPartial + indexTypes = append(indexTypes, indexInfo.valueType) } + } else if isObjectLiteralType(t) && t.objectFlags&ObjectFlagsContainsSpread == 0 { + checkFlags |= ast.CheckFlagsWritePartial + indexTypes = append(indexTypes, c.undefinedType) + } else { + checkFlags |= ast.CheckFlagsReadPartial } } } From 4e2bbf0c7f6fa45f14b8edcd20453843663dc65e Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 29 Oct 2026 15:01:46 -0700 Subject: [PATCH 4/5] Look up contextual properties without mapping non-union types getTypeOfPropertyOfContextualType runs for every property of every contextually typed object literal. It passed a closure to mapTypeEx even though the contextual type is usually not a union, so the closure was allocated only to be called once. Handle non-union types directly and keep mapTypeEx for unions. Also, as with getApplicableIndexInfoForName, don't create a string literal type for the property name when the contextual type has no index signatures that it could be matched against. Co-authored-by: Steven Chong <25894545+teamchong@users.noreply.github.com> --- tsc/internal/checker/checker.go | 99 +++++++++++++++++++-------------- 1 file changed, 57 insertions(+), 42 deletions(-) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index df57fafaad277..17ceebeefc398 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -31403,55 +31403,66 @@ func (c *Checker) getTypeOfPropertyOfContextualType(t *Type, name string) *Type } func (c *Checker) getTypeOfPropertyOfContextualTypeEx(t *Type, name string, nameType *Type) *Type { + if t.flags&TypeFlagsUnion == 0 { + // Avoid allocating a closure for mapType when there is a single type to look in. + if t.flags&TypeFlagsNever != 0 { + return t + } + return c.getTypeOfPropertyOfContextualTypeWorker(t, name, nameType) + } return c.mapTypeEx(t, func(t *Type) *Type { - if t.flags&TypeFlagsIntersection != 0 { - var types []*Type - var indexInfoCandidates []*Type - ignoreIndexInfos := false - for _, constituentType := range t.Types() { - if constituentType.flags&TypeFlagsObject == 0 { - continue - } - if c.isGenericMappedType(constituentType) && c.getMappedTypeNameTypeKind(constituentType) != MappedTypeNameTypeKindRemapping { - substitutedType := c.getIndexedMappedTypeSubstitutedTypeOfContextualType(constituentType, name, nameType) - types = c.appendContextualPropertyTypeConstituent(types, substitutedType) - continue - } - propertyType := c.getTypeOfConcretePropertyOfContextualType(constituentType, name) - if propertyType == nil { - if !ignoreIndexInfos { - indexInfoCandidates = append(indexInfoCandidates, constituentType) - } - continue - } - ignoreIndexInfos = true - indexInfoCandidates = nil - types = c.appendContextualPropertyTypeConstituent(types, propertyType) - } - for _, candidate := range indexInfoCandidates { - indexInfoType := c.getTypeFromIndexInfosOfContextualType(candidate, name, nameType) - types = c.appendContextualPropertyTypeConstituent(types, indexInfoType) + return c.getTypeOfPropertyOfContextualTypeWorker(t, name, nameType) + }, true /*noReductions*/) +} + +func (c *Checker) getTypeOfPropertyOfContextualTypeWorker(t *Type, name string, nameType *Type) *Type { + if t.flags&TypeFlagsIntersection != 0 { + var types []*Type + var indexInfoCandidates []*Type + ignoreIndexInfos := false + for _, constituentType := range t.Types() { + if constituentType.flags&TypeFlagsObject == 0 { + continue } - if len(types) == 0 { - return nil + if c.isGenericMappedType(constituentType) && c.getMappedTypeNameTypeKind(constituentType) != MappedTypeNameTypeKindRemapping { + substitutedType := c.getIndexedMappedTypeSubstitutedTypeOfContextualType(constituentType, name, nameType) + types = c.appendContextualPropertyTypeConstituent(types, substitutedType) + continue } - if len(types) == 1 { - return types[0] + propertyType := c.getTypeOfConcretePropertyOfContextualType(constituentType, name) + if propertyType == nil { + if !ignoreIndexInfos { + indexInfoCandidates = append(indexInfoCandidates, constituentType) + } + continue } - return c.getIntersectionType(types) + ignoreIndexInfos = true + indexInfoCandidates = nil + types = c.appendContextualPropertyTypeConstituent(types, propertyType) } - if t.flags&TypeFlagsObject == 0 { - return nil + for _, candidate := range indexInfoCandidates { + indexInfoType := c.getTypeFromIndexInfosOfContextualType(candidate, name, nameType) + types = c.appendContextualPropertyTypeConstituent(types, indexInfoType) } - if c.isGenericMappedType(t) && c.getMappedTypeNameTypeKind(t) != MappedTypeNameTypeKindRemapping { - return c.getIndexedMappedTypeSubstitutedTypeOfContextualType(t, name, nameType) + if len(types) == 0 { + return nil } - result := c.getTypeOfConcretePropertyOfContextualType(t, name) - if result != nil { - return result + if len(types) == 1 { + return types[0] } - return c.getTypeFromIndexInfosOfContextualType(t, name, nameType) - }, true /*noReductions*/) + return c.getIntersectionType(types) + } + if t.flags&TypeFlagsObject == 0 { + return nil + } + if c.isGenericMappedType(t) && c.getMappedTypeNameTypeKind(t) != MappedTypeNameTypeKindRemapping { + return c.getIndexedMappedTypeSubstitutedTypeOfContextualType(t, name, nameType) + } + result := c.getTypeOfConcretePropertyOfContextualType(t, name) + if result != nil { + return result + } + return c.getTypeFromIndexInfosOfContextualType(t, name, nameType) } func (c *Checker) getIndexedMappedTypeSubstitutedTypeOfContextualType(t *Type, name string, nameType *Type) *Type { @@ -31500,10 +31511,14 @@ func (c *Checker) getTypeFromIndexInfosOfContextualType(t *Type, name string, na return restType } } + indexInfos := c.getIndexInfosOfStructuredType(t) + if len(indexInfos) == 0 { + return nil + } if nameType == nil { nameType = c.getStringLiteralType(name) } - indexInfo := c.findApplicableIndexInfo(c.getIndexInfosOfStructuredType(t), nameType) + indexInfo := c.findApplicableIndexInfo(indexInfos, nameType) if indexInfo == nil { return nil } From b1d92bf633eff9b7d9f9b4e8175e761e5eba4527 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 29 Oct 2026 15:05:05 -0700 Subject: [PATCH 5/5] Only clone class static members when something is inherited Resolving the members of a derived class's constructor type cloned the class's exports table so that static members of the base constructor could be added to it. But the base constructor's properties are often all shadowed: every class has its own 'prototype', and many bases have no other statics. In that case the clone was identical to the original and the members were stored a second time for nothing. Find the first base property that would actually be inherited and only clone the table (and reset the type's members) when there is one. The base constructor's properties are still resolved at the same point, so resolution order is unchanged. Co-authored-by: Steven Chong <25894545+teamchong@users.noreply.github.com> --- tsc/internal/checker/checker.go | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 17ceebeefc398..55324d265b194 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -20200,18 +20200,25 @@ func (c *Checker) getTypeWithThisArgument(t *Type, thisArgument *Type, needAppar func (c *Checker) addInheritedMembers(symbols ast.SymbolTable, baseSymbols []*ast.Symbol) ast.SymbolTable { for _, base := range baseSymbols { - if !isStaticPrivateIdentifierProperty(base) { - if s, ok := symbols.Lookup(base.Name()); !ok || s.Flags()&ast.SymbolFlagsValue == 0 { - if symbols == nil { - symbols = ast.NewSymbolTable() - } - symbols.Set(base.Name(), base) + if isInheritedMember(symbols, base) { + if symbols == nil { + symbols = ast.NewSymbolTable() } + symbols.Set(base.Name(), base) } } return symbols } +// Reports whether addInheritedMembers would add the base symbol to the given symbols. +func isInheritedMember(symbols ast.SymbolTable, base *ast.Symbol) bool { + if isStaticPrivateIdentifierProperty(base) { + return false + } + s, ok := symbols.Lookup(base.Name()) + return !ok || s.Flags()&ast.SymbolFlagsValue == 0 +} + func (c *Checker) resolveDeclaredMembers(t *Type) *InterfaceType { d := t.AsInterfaceType() if !d.declaredMembersResolved { @@ -21289,9 +21296,14 @@ func (c *Checker) resolveAnonymousTypeMembers(t *Type) { classType := c.getDeclaredTypeOfClassOrInterface(symbol) baseConstructorType := c.getBaseConstructorTypeOfClass(classType) if baseConstructorType.flags&(TypeFlagsObject|TypeFlagsIntersection|TypeFlagsTypeVariable) != 0 { - members = members.Clone() - c.addInheritedMembers(members, c.getPropertiesOfType(baseConstructorType)) - c.setStructuredTypeMembers(t, members, nil, nil, nil) + // Base static members are usually all shadowed (e.g. 'prototype'), in which case + // the exports table can be used as is. + baseProps := c.getPropertiesOfType(baseConstructorType) + if i := slices.IndexFunc(baseProps, func(base *ast.Symbol) bool { return isInheritedMember(members, base) }); i >= 0 { + members = members.Clone() + c.addInheritedMembers(members, baseProps[i:]) + c.setStructuredTypeMembers(t, members, nil, nil, nil) + } } else if baseConstructorType == c.anyType { baseConstructorIndexInfo = c.anyBaseTypeIndexInfo }