From 670f2e8d728a3a99c412ca674047623dd2fd0a59 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 29 Oct 2026 12:46:17 -0700 Subject: [PATCH 1/3] Skip inferring into targets without type variables Inferring to a target that cannot contain type variables is a no-op: inferFromTypes bails out immediately. Several callers nevertheless did all the work of producing the source side of such a pair before discovering this, e.g. resolving the source parameter or property type, or the source signatures and index infos. Check the target first in applyToParameterTypes, applyToReturnTypes, and inferFromProperties, and only compute the source type when the target could receive inferences. Likewise, look up target signatures and index infos before the source's, and stop early when there are none, since nothing would be inferred from the source anyway. Co-authored-by: Steven Chong <25894545+teamchong@users.noreply.github.com> --- tsc/internal/checker/inference.go | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/tsc/internal/checker/inference.go b/tsc/internal/checker/inference.go index c05ec5fed336f..7a467d88a73d0 100644 --- a/tsc/internal/checker/inference.go +++ b/tsc/internal/checker/inference.go @@ -841,19 +841,25 @@ func (c *Checker) inferFromProperties(n *InferenceState, source *Type, target *T for _, targetProp := range properties { sourceProp := c.getPropertyOfType(source, targetProp.Name()) if sourceProp != nil && !core.Some(sourceProp.Declarations(), c.isSkipDirectInferenceNode) { - c.inferFromTypes(n, c.removeMissingType(c.getTypeOfSymbol(sourceProp), sourceProp.Flags()&ast.SymbolFlagsOptional != 0), c.removeMissingType(c.getTypeOfSymbol(targetProp), targetProp.Flags()&ast.SymbolFlagsOptional != 0)) + targetType := c.removeMissingType(c.getTypeOfSymbol(targetProp), targetProp.Flags()&ast.SymbolFlagsOptional != 0) + if c.couldContainTypeVariables(targetType) { + c.inferFromTypes(n, c.removeMissingType(c.getTypeOfSymbol(sourceProp), sourceProp.Flags()&ast.SymbolFlagsOptional != 0), targetType) + } } } } func (c *Checker) inferFromSignatures(n *InferenceState, source *Type, target *Type, kind SignatureKind) { + targetSignatures := c.getSignaturesOfType(target, kind) + targetLen := len(targetSignatures) + if targetLen == 0 { + return + } sourceSignatures := c.getSignaturesOfType(source, kind) sourceLen := len(sourceSignatures) if sourceLen > 0 { // We match source and target signatures from the bottom up, and if the source has fewer signatures // than the target, we infer from the first source signature to the excess target signatures. - targetSignatures := c.getSignaturesOfType(target, kind) - targetLen := len(targetSignatures) for i := range targetLen { sourceIndex := max(sourceLen-targetLen+i, 0) c.inferFromSignature(n, c.getBaseSignature(sourceSignatures[sourceIndex]), c.getErasedSignature(targetSignatures[i])) @@ -889,17 +895,21 @@ func (c *Checker) applyToParameterTypes(source *Signature, target *Signature, ca if sourceRestType == nil { paramCount = min(sourceCount, targetNonRestCount) } + // Callers only use the callback to infer to the target type, which is a no-op when the target cannot + // contain type variables, so we avoid computing the corresponding source types in that case. sourceThisType := c.getThisTypeOfSignature(source) if sourceThisType != nil { targetThisType := c.getThisTypeOfSignature(target) - if targetThisType != nil { + if targetThisType != nil && c.couldContainTypeVariables(targetThisType) { callback(sourceThisType, targetThisType) } } for i := range paramCount { - callback(c.getTypeAtPosition(source, i), c.getTypeAtPosition(target, i)) + if targetType := c.getTypeAtPosition(target, i); c.couldContainTypeVariables(targetType) { + callback(c.getTypeAtPosition(source, i), targetType) + } } - if targetRestType != nil { + if targetRestType != nil && c.couldContainTypeVariables(targetRestType) { callback(c.getRestTypeAtPosition(source, paramCount, c.isConstTypeVariable(targetRestType, 0) && !someType(targetRestType, c.isMutableArrayLikeType) /*readonly*/), targetRestType) } } @@ -909,7 +919,9 @@ func (c *Checker) applyToReturnTypes(source *Signature, target *Signature, callb if targetTypePredicate != nil { sourceTypePredicate := c.getTypePredicateOfSignature(source) if sourceTypePredicate != nil && c.typePredicateKindsMatch(sourceTypePredicate, targetTypePredicate) && sourceTypePredicate.t != nil && targetTypePredicate.t != nil { - callback(sourceTypePredicate.t, targetTypePredicate.t) + if c.couldContainTypeVariables(targetTypePredicate.t) { + callback(sourceTypePredicate.t, targetTypePredicate.t) + } return } } @@ -926,6 +938,9 @@ func (c *Checker) inferFromIndexTypes(n *InferenceState, source *Type, target *T priority = InferencePriorityHomomorphicMappedType } indexInfos := c.getIndexInfosOfType(target) + if len(indexInfos) == 0 { + return + } if c.isObjectTypeWithInferableIndex(source) { for _, targetInfo := range indexInfos { var propTypes []*Type From 6ee8b84da95d7dd0276733c0ad886cbea574be1b Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 29 Oct 2026 12:46:31 -0700 Subject: [PATCH 2/3] Reuse candidate slices when inference priority improves When a higher priority inference is made for a type parameter, its existing candidates are discarded. Setting the slices to nil threw away their backing arrays, so the following appends had to allocate and grow new ones from scratch. Truncate the slices instead so the existing capacity is reused. The old elements are cleared first so the discarded candidate types are not kept alive by the backing array. Candidate slices are never shared between inference infos (clones copy them), so reusing them in place is safe. Co-authored-by: Steven Chong <25894545+teamchong@users.noreply.github.com> --- tsc/internal/checker/inference.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tsc/internal/checker/inference.go b/tsc/internal/checker/inference.go index 7a467d88a73d0..a5e3b6932517a 100644 --- a/tsc/internal/checker/inference.go +++ b/tsc/internal/checker/inference.go @@ -182,8 +182,10 @@ func (c *Checker) inferFromTypes(n *InferenceState, source *Type, target *Type) return } if n.priority < inference.priority { - inference.candidates = nil - inference.contraCandidates = nil + clear(inference.candidates) + inference.candidates = inference.candidates[:0] + clear(inference.contraCandidates) + inference.contraCandidates = inference.contraCandidates[:0] inference.topLevel = true inference.priority = n.priority } From 2c96ce5dacc1816acd5971d03afc33e9c69ffabd Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 29 Oct 2026 13:17:55 -0700 Subject: [PATCH 3/3] Use stack scratch buffers in inference helpers Several inference helpers build small temporary slices that never outlive the call: the matched sources and targets in inferFromMatchingTypes, the single-element source list, matched flags, and unmatched sources in inferToMultipleTypes, and the true/false branch pair in inferToConditionalType. These run for nearly every union and conditional type inference, and each one cost a heap allocation. Back these slices with small fixed-size arrays so the common cases stay on the stack, falling back to the heap only when they grow past the buffer. Co-authored-by: Steven Chong <25894545+teamchong@users.noreply.github.com> --- tsc/internal/checker/inference.go | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/tsc/internal/checker/inference.go b/tsc/internal/checker/inference.go index a5e3b6932517a..5d798938fe726 100644 --- a/tsc/internal/checker/inference.go +++ b/tsc/internal/checker/inference.go @@ -381,8 +381,9 @@ func (c *Checker) invokeOnce(n *InferenceState, source *Type, target *Type, acti } func (c *Checker) inferFromMatchingTypes(n *InferenceState, sources []*Type, targets []*Type, matches func(c *Checker, s *Type, t *Type) bool, sort bool) ([]*Type, []*Type) { - var matchedSources []*Type - var matchedTargets []*Type + var matchedSourcesBuf, matchedTargetsBuf [8]*Type + matchedSources := matchedSourcesBuf[:0] + matchedTargets := matchedTargetsBuf[:0] for _, t := range targets { for _, s := range sources { if matches(c, s, t) { @@ -462,13 +463,21 @@ func (c *Checker) inferToMultipleTypes(n *InferenceState, source *Type, targets typeVariableCount := 0 if targetFlags&TypeFlagsUnion != 0 { var nakedTypeVariable *Type + var sourceBuf [1]*Type var sources []*Type if source.flags&TypeFlagsUnion != 0 { sources = source.Types() } else { - sources = []*Type{source} + sourceBuf[0] = source + sources = sourceBuf[:] + } + var matchedBuf [64]bool + var matched []bool + if len(sources) <= len(matchedBuf) { + matched = matchedBuf[:len(sources)] + } else { + matched = make([]bool, len(sources)) } - matched := make([]bool, len(sources)) inferenceCircularity := false // First infer to types that are not naked type variables. For each source type we // track whether inferences were made from that particular type to some target with @@ -506,7 +515,8 @@ func (c *Checker) inferToMultipleTypes(n *InferenceState, source *Type, targets // types from which no inferences have been made so far and infer from that union to the // naked type variable. if typeVariableCount == 1 && !inferenceCircularity { - var unmatched []*Type + var unmatchedBuf [8]*Type + unmatched := unmatchedBuf[:0] for i, s := range sources { if !matched[i] { unmatched = append(unmatched, s) @@ -571,8 +581,8 @@ func (c *Checker) inferToConditionalType(n *InferenceState, source *Type, target c.inferFromTypes(n, getNonDistributedTypeParameter(c.getTrueTypeFromConditionalType(source)), c.getTrueTypeFromConditionalType(target)) c.inferFromTypes(n, getNonDistributedTypeParameter(c.getFalseTypeFromConditionalType(source)), c.getFalseTypeFromConditionalType(target)) } else { - targetTypes := []*Type{c.getTrueTypeFromConditionalType(target), c.getFalseTypeFromConditionalType(target)} - c.inferToMultipleTypesWithPriority(n, source, targetTypes, target.flags, core.IfElse(n.contravariant, InferencePriorityContravariantConditional, 0)) + targetTypes := [2]*Type{c.getTrueTypeFromConditionalType(target), c.getFalseTypeFromConditionalType(target)} + c.inferToMultipleTypesWithPriority(n, source, targetTypes[:], target.flags, core.IfElse(n.contravariant, InferencePriorityContravariantConditional, 0)) } }