From 166bb41022d3f10ac16638f6181217c6cad2f8e7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 29 Oct 2026 21:31:32 +0000 Subject: [PATCH 1/2] fix(64998): defer index signature value types for keyof circularity Declaration emit of `type Status = keyof StatusClass | ""` (with StatusClass members and a string index signature typed as Status) collapsed to `any` when status.ts and consumer.ts were assigned to different checkers. keyof only needs index key types, but getIndexInfosOfIndexSymbol eagerly resolved value types, and class instantiation (via the implicit `this` type) re-resolved them during Status's own alias resolution, incorrectly marking Status circular. Defer value-type resolution until first use, preserving mappers across instantiation. Fixes microsoft/TypeScript#64998 Co-authored-by: timurzvural --- tsc/internal/api/session.go | 2 +- tsc/internal/checker/checker.go | 93 +++++++++++++------ tsc/internal/checker/exports.go | 4 + tsc/internal/checker/flow.go | 2 +- tsc/internal/checker/inference.go | 10 +- tsc/internal/checker/jsx.go | 2 +- tsc/internal/checker/nodebuilder_hover.go | 2 +- tsc/internal/checker/nodebuilderimpl.go | 13 ++- tsc/internal/checker/relater.go | 12 +-- tsc/internal/checker/types.go | 3 + .../declaration_checkers_64998_test.go | 78 ++++++++++++++++ 11 files changed, 175 insertions(+), 46 deletions(-) create mode 100644 tsc/internal/compiler/declaration_checkers_64998_test.go diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index a434123b2b54d..1f28ad218696a 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -764,7 +764,7 @@ func (setup checkerSetup) newIndexInfoResponse(info *checker.IndexInfo) *IndexIn } result := &IndexInfoResponse{ KeyType: *setup.sd.newTypeResponse(setup.projectID, info.KeyType(), setup.checker), - ValueType: *setup.sd.newTypeResponse(setup.projectID, info.ValueType(), setup.checker), + ValueType: *setup.sd.newTypeResponse(setup.projectID, setup.checker.GetValueTypeOfIndexInfo(info), setup.checker), IsReadonly: info.IsReadonly(), } if info.Declaration() != nil { diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index d9cb44e00a903..62360353e1226 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -4957,8 +4957,8 @@ func (c *Checker) checkIndexConstraintForProperty(t *Type, prop *ast.Symbol, pro }) { errorNode = interfaceDeclaration } - if errorNode != nil && !c.isTypeAssignableTo(propType, info.valueType) { - diagnostic := NewDiagnosticForNode(errorNode, diagnostics.Property_0_of_type_1_is_not_assignable_to_2_index_type_3, c.symbolToString(prop), c.TypeToString(propType), c.TypeToString(info.keyType), c.TypeToString(info.valueType)) + if errorNode != nil && !c.isTypeAssignableTo(propType, c.getValueTypeOfIndexInfo(info)) { + diagnostic := NewDiagnosticForNode(errorNode, diagnostics.Property_0_of_type_1_is_not_assignable_to_2_index_type_3, c.symbolToString(prop), c.TypeToString(propType), c.TypeToString(info.keyType), c.TypeToString(c.getValueTypeOfIndexInfo(info))) if propDeclaration != nil && errorNode != propDeclaration { diagnostic.AddRelatedInfo(NewDiagnosticForNode(propDeclaration, diagnostics.X_0_is_declared_here, c.symbolToString(prop))) } @@ -4998,8 +4998,8 @@ func (c *Checker) checkIndexConstraintForIndexSignature(t *Type, checkInfo *Inde }) { errorNode = interfaceDeclaration } - if errorNode != nil && !c.isTypeAssignableTo(checkInfo.valueType, info.valueType) { - c.error(errorNode, diagnostics.X_0_index_type_1_is_not_assignable_to_2_index_type_3, c.TypeToString(checkInfo.keyType), c.TypeToString(checkInfo.valueType), c.TypeToString(info.keyType), c.TypeToString(info.valueType)) + if errorNode != nil && !c.isTypeAssignableTo(c.getValueTypeOfIndexInfo(checkInfo), c.getValueTypeOfIndexInfo(info)) { + c.error(errorNode, diagnostics.X_0_index_type_1_is_not_assignable_to_2_index_type_3, c.TypeToString(checkInfo.keyType), c.TypeToString(c.getValueTypeOfIndexInfo(checkInfo)), c.TypeToString(info.keyType), c.TypeToString(c.getValueTypeOfIndexInfo(info))) } } } @@ -11598,7 +11598,7 @@ func (c *Checker) checkPropertyAccessExpressionOrQualifiedName(node *ast.Node, l if indexInfo.isReadonly && (ast.IsAssignmentTarget(node) || isDeleteTarget(node)) { c.error(node, diagnostics.Index_signature_in_type_0_only_permits_reading, c.TypeToString(apparentType)) } - propType = indexInfo.valueType + propType = c.getValueTypeOfIndexInfo(indexInfo) if c.compilerOptions.NoUncheckedIndexedAccess == core.TSTrue && getAssignmentTargetKind(node) != AssignmentKindDefinite { propType = c.getUnionType([]*Type{propType, c.missingType}) } @@ -13758,7 +13758,7 @@ func (c *Checker) getSpreadType(left *Type, right *Type, symbol *ast.Symbol, obj func (c *Checker) getIndexInfoWithReadonly(info *IndexInfo, readonly bool) *IndexInfo { if info.isReadonly != readonly { - return c.newIndexInfo(info.keyType, info.valueType, readonly, info.declaration, info.components) + return c.newIndexInfo(info.keyType, c.getValueTypeOfIndexInfo(info), readonly, info.declaration, info.components) } return info } @@ -18872,7 +18872,7 @@ func (c *Checker) getWidenedTypeOfObjectLiteral(t *Type, context *WideningContex } } result := c.newAnonymousType(t.symbol, members, nil, nil, core.SameMap(c.getIndexInfosOfType(t), func(info *IndexInfo) *IndexInfo { - return c.newIndexInfo(info.keyType, c.getWidenedType(info.valueType), info.isReadonly, info.declaration, info.components) + return c.newIndexInfo(info.keyType, c.getWidenedType(c.getValueTypeOfIndexInfo(info)), info.isReadonly, info.declaration, info.components) })) // Retain js literal flag through widening result.objectFlags |= t.objectFlags & (ObjectFlagsJSLiteral | ObjectFlagsNonInferrableType) @@ -19480,7 +19480,7 @@ func (c *Checker) getIndexInfoOfType(t *Type, keyType *Type) *IndexInfo { func (c *Checker) getIndexTypeOfType(t *Type, keyType *Type) *Type { info := c.getIndexInfoOfType(t, keyType) if info != nil { - return info.valueType + return c.getValueTypeOfIndexInfo(info) } return nil } @@ -19529,7 +19529,7 @@ func (c *Checker) findApplicableIndexInfo(indexInfos []*IndexInfo, keyType *Type isReadonly := true types := make([]*Type, len(applicableInfos)) for i, info := range applicableInfos { - types[i] = info.valueType + types[i] = c.getValueTypeOfIndexInfo(info) if !info.isReadonly { isReadonly = false } @@ -20225,12 +20225,18 @@ func (c *Checker) getIndexInfosOfIndexSymbol(indexSymbol *ast.Symbol, siblingSym if len(parameters) == 1 { typeNode := parameters[0].Type() if typeNode != nil { - valueType := c.anyType - if returnTypeNode != nil { - valueType = c.getTypeFromTypeNode(returnTypeNode) - } + // Defer resolving the index signature's value type. Eager resolution is + // problematic for patterns like `type T = keyof C` where C declares + // `[key: K]: T` — computing keyof only needs key types, and resolving the + // value type while T is still resolving incorrectly marks T circular + // (see microsoft/TypeScript#64998). The value type is resolved lazily via + // getValueTypeOfIndexInfo when first needed. forEachType(c.getTypeFromTypeNode(typeNode), func(keyType *Type) { if c.isValidIndexKeyType(keyType) && findIndexInfo(indexInfos, keyType) == nil { + var valueType *Type + if returnTypeNode == nil { + valueType = c.anyType + } indexInfo := c.newIndexInfo(keyType, valueType, ast.HasModifier(declaration, ast.ModifierFlagsReadonly), declaration, nil) indexInfos = append(indexInfos, indexInfo) } @@ -21218,8 +21224,23 @@ func (c *Checker) instantiateSignatureEx(sig *Signature, m *TypeMapper, eraseTyp } func (c *Checker) instantiateIndexInfo(info *IndexInfo, m *TypeMapper) *IndexInfo { - newValueType := c.instantiateType(info.valueType, m) - if newValueType == info.valueType { + // Keep deferred value types deferred across instantiation. Eagerly resolving the + // value type here reintroduces circularity for `type T = keyof C` where C has an + // index signature typed as T — classes always instantiate index infos because of + // the implicit `this` type (microsoft/TypeScript#64998). + if info.valueType == nil && info.declaration != nil { + keyType := c.instantiateType(info.keyType, m) + combined := c.combineTypeMappers(info.mapper, m) + if keyType == info.keyType && combined == info.mapper { + return info + } + result := c.newIndexInfo(keyType, nil, info.isReadonly, info.declaration, info.components) + result.mapper = combined + return result + } + valueType := c.getValueTypeOfIndexInfo(info) + newValueType := c.instantiateType(valueType, m) + if newValueType == valueType { return info } return c.newIndexInfo(info.keyType, newValueType, info.isReadonly, info.declaration, info.components) @@ -21932,10 +21953,10 @@ func (c *Checker) appendIndexInfo(indexInfos []*IndexInfo, newInfo *IndexInfo, u var valueType *Type var isReadonly bool if union { - valueType = c.getUnionType([]*Type{info.valueType, newInfo.valueType}) + valueType = c.getUnionType([]*Type{c.getValueTypeOfIndexInfo(info), c.getValueTypeOfIndexInfo(newInfo)}) isReadonly = info.isReadonly || newInfo.isReadonly } else { - valueType = c.getIntersectionType([]*Type{info.valueType, newInfo.valueType}) + valueType = c.getIntersectionType([]*Type{c.getValueTypeOfIndexInfo(info), c.getValueTypeOfIndexInfo(newInfo)}) isReadonly = info.isReadonly && newInfo.isReadonly } indexInfos[i] = c.newIndexInfo(info.keyType, valueType, isReadonly, nil, nil) @@ -22129,7 +22150,7 @@ func (c *Checker) createUnionOrIntersectionProperty(containingType *Type, name s } indexTypes = append(indexTypes, indexType) } else { - indexTypes = append(indexTypes, indexInfo.valueType) + indexTypes = append(indexTypes, c.getValueTypeOfIndexInfo(indexInfo)) } } else if isObjectLiteralType(t) && t.objectFlags&ObjectFlagsContainsSpread == 0 { checkFlags |= ast.CheckFlagsWritePartial @@ -26010,6 +26031,26 @@ func (c *Checker) newIndexInfo(keyType *Type, valueType *Type, isReadonly bool, return info } +// getValueTypeOfIndexInfo returns the value type of an index signature, resolving it +// lazily from the declaration when it was deferred by getIndexInfosOfIndexSymbol. +func (c *Checker) getValueTypeOfIndexInfo(info *IndexInfo) *Type { + if info.valueType != nil { + return info.valueType + } + if info.declaration != nil { + if returnTypeNode := info.declaration.Type(); returnTypeNode != nil { + // Assign a temporary non-nil value before resolving to avoid recursive + // re-entry through the same IndexInfo while the value type is circularly + // referenced from the type being resolved (e.g. keyof). + info.valueType = c.unresolvedType + info.valueType = c.instantiateType(c.getTypeFromTypeNode(returnTypeNode), info.mapper) + return info.valueType + } + } + info.valueType = c.anyType + return info.valueType +} + func (c *Checker) getRegularTypeOfLiteralType(t *Type) *Type { if t.flags&TypeFlagsFreshable != 0 { return t.AsLiteralType().regularType @@ -27862,9 +27903,9 @@ func (c *Checker) getPropertyTypeForIndexType(originalObjectType *Type, objectTy indexNode := getIndexNodeForAccessExpression(accessNode) c.error(indexNode, diagnostics.Type_0_cannot_be_used_as_an_index_type, c.TypeToString(indexType)) if accessFlags&AccessFlagsIncludeUndefined != 0 { - return c.getUnionType([]*Type{indexInfo.valueType, c.missingType}) + return c.getUnionType([]*Type{c.getValueTypeOfIndexInfo(indexInfo), c.missingType}) } else { - return indexInfo.valueType + return c.getValueTypeOfIndexInfo(indexInfo) } } c.errorIfWritingToReadonlyIndex(indexInfo, objectType, accessExpression) @@ -27877,9 +27918,9 @@ func (c *Checker) getPropertyTypeForIndexType(originalObjectType *Type, objectTy (indexType.symbol != nil && indexType.flags&TypeFlagsEnumLiteral != 0 && c.getParentOfSymbol(indexType.symbol) == objectType.symbol)) { - return c.getUnionType([]*Type{indexInfo.valueType, c.missingType}) + return c.getUnionType([]*Type{c.getValueTypeOfIndexInfo(indexInfo), c.missingType}) } - return indexInfo.valueType + return c.getValueTypeOfIndexInfo(indexInfo) } if indexType.flags&TypeFlagsNever != 0 { return c.neverType @@ -30113,7 +30154,7 @@ func (c *Checker) getTypeOfPropertyOrIndexSignatureOfType(t *Type, name string) } indexInfo := c.getApplicableIndexInfoForName(t, name) if indexInfo != nil { - return c.addOptionalityEx(indexInfo.valueType, true /*isProperty*/, true /*isOptional*/) + return c.addOptionalityEx(c.getValueTypeOfIndexInfo(indexInfo), true /*isProperty*/, true /*isOptional*/) } return nil } @@ -30747,7 +30788,7 @@ func (c *Checker) getContextualTypeForObjectLiteralElement(element *ast.Node, co if indexInfo == nil { return nil } - return indexInfo.valueType + return c.getValueTypeOfIndexInfo(indexInfo) }, true /*noReductions*/) } } @@ -31453,7 +31494,7 @@ func (c *Checker) getTypeFromIndexInfosOfContextualType(t *Type, name string, na if indexInfo == nil { return nil } - return indexInfo.valueType + return c.getValueTypeOfIndexInfo(indexInfo) } func (c *Checker) isCircularMappedProperty(symbol *ast.Symbol) bool { @@ -32903,7 +32944,7 @@ func (c *Checker) getApplicableIndexSymbol(t *Type, keyType *Type) *ast.Symbol { symbol.SetValueDeclaration(declarations[0]) symbol.SetParent(t.symbol) links := c.valueSymbolLinks.Get(symbol) - links.resolvedType = info.valueType + links.resolvedType = c.getValueTypeOfIndexInfo(info) info.indexSymbol = symbol } } diff --git a/tsc/internal/checker/exports.go b/tsc/internal/checker/exports.go index f03be066141dc..ab4c58ee10112 100644 --- a/tsc/internal/checker/exports.go +++ b/tsc/internal/checker/exports.go @@ -353,6 +353,10 @@ func (c *Checker) GetIndexInfoOfType(t *Type, keyType *Type) *IndexInfo { return c.getIndexInfoOfType(t, keyType) } +func (c *Checker) GetValueTypeOfIndexInfo(info *IndexInfo) *Type { + return c.getValueTypeOfIndexInfo(info) +} + func (c *Checker) GetIndexTypeOfType(t *Type, keyType *Type) *Type { return c.getIndexTypeOfType(t, keyType) } diff --git a/tsc/internal/checker/flow.go b/tsc/internal/checker/flow.go index d2831231772fe..e27cb159f9985 100644 --- a/tsc/internal/checker/flow.go +++ b/tsc/internal/checker/flow.go @@ -2372,7 +2372,7 @@ func (c *Checker) getTypeOfDestructuredProperty(t *Type, name *ast.Node) *Type { return propType } if indexInfo := c.getApplicableIndexInfoForName(t, text); indexInfo != nil { - return c.includeUndefinedInIndexSignature(indexInfo.valueType) + return c.includeUndefinedInIndexSignature(c.getValueTypeOfIndexInfo(indexInfo)) } return c.errorType } diff --git a/tsc/internal/checker/inference.go b/tsc/internal/checker/inference.go index c05ec5fed336f..c169b67b334da 100644 --- a/tsc/internal/checker/inference.go +++ b/tsc/internal/checker/inference.go @@ -940,18 +940,18 @@ func (c *Checker) inferFromIndexTypes(n *InferenceState, source *Type, target *T } for _, info := range c.getIndexInfosOfType(source) { if c.isApplicableIndexType(info.keyType, targetInfo.keyType) { - propTypes = append(propTypes, info.valueType) + propTypes = append(propTypes, c.getValueTypeOfIndexInfo(info)) } } if len(propTypes) != 0 { - c.inferWithPriority(n, c.getUnionType(propTypes), targetInfo.valueType, priority) + c.inferWithPriority(n, c.getUnionType(propTypes), c.getValueTypeOfIndexInfo(targetInfo), priority) } } } for _, targetInfo := range indexInfos { sourceInfo := c.getApplicableIndexInfo(source, targetInfo.keyType) if sourceInfo != nil { - c.inferWithPriority(n, sourceInfo.valueType, targetInfo.valueType, priority) + c.inferWithPriority(n, c.getValueTypeOfIndexInfo(sourceInfo), c.getValueTypeOfIndexInfo(targetInfo), priority) } } } @@ -998,7 +998,7 @@ func (c *Checker) inferToMappedType(n *InferenceState, source *Type, target *Typ propTypes := core.Map(c.getPropertiesOfType(source), c.getTypeOfSymbol) indexTypes := core.Map(c.getIndexInfosOfType(source), func(info *IndexInfo) *Type { if info != c.enumNumberIndexInfo { - return info.valueType + return c.getValueTypeOfIndexInfo(info) } return c.neverType }) @@ -1115,7 +1115,7 @@ func (c *Checker) resolveReverseMappedTypeMembers(t *Type) { optionalMask := core.IfElse(modifiers&MappedTypeModifiersIncludeOptional != 0, 0, ast.SymbolFlagsOptional) var indexInfos []*IndexInfo if indexInfo != nil { - indexInfos = []*IndexInfo{c.newIndexInfo(c.stringType, core.OrElse(c.inferReverseMappedType(indexInfo.valueType, r.mappedType, r.constraintType), c.unknownType), readonlyMask && indexInfo.isReadonly, nil, nil)} + indexInfos = []*IndexInfo{c.newIndexInfo(c.stringType, core.OrElse(c.inferReverseMappedType(c.getValueTypeOfIndexInfo(indexInfo), r.mappedType, r.constraintType), c.unknownType), readonlyMask && indexInfo.isReadonly, nil, nil)} } members := ast.NewSymbolTable() limitedConstraint := c.getLimitedConstraint(t) diff --git a/tsc/internal/checker/jsx.go b/tsc/internal/checker/jsx.go index 1054f4d46f8c5..ebc2e494fd4f7 100644 --- a/tsc/internal/checker/jsx.go +++ b/tsc/internal/checker/jsx.go @@ -1199,7 +1199,7 @@ func (c *Checker) getIntrinsicAttributesTypeFromJsxOpeningLikeElement(node *ast. if links.jsxFlags&JsxFlagsIntrinsicIndexedElement != 0 { indexInfo := c.getApplicableIndexInfoForName(c.getJsxType(JsxNames.IntrinsicElements, node), node.TagName().Text()) if indexInfo != nil { - links.resolvedJsxElementAttributesType = indexInfo.valueType + links.resolvedJsxElementAttributesType = c.getValueTypeOfIndexInfo(indexInfo) return links.resolvedJsxElementAttributesType } } diff --git a/tsc/internal/checker/nodebuilder_hover.go b/tsc/internal/checker/nodebuilder_hover.go index b49ac981fbd41..1e329974f26a7 100644 --- a/tsc/internal/checker/nodebuilder_hover.go +++ b/tsc/internal/checker/nodebuilder_hover.go @@ -371,7 +371,7 @@ func (b *NodeBuilderImpl) serializeIndexSignaturesOfType(input *Type, baseType * for _, info := range b.ch.getIndexInfosOfType(input) { if baseType != nil { baseInfo := b.ch.getIndexInfoOfType(baseType, info.keyType) - if baseInfo != nil && b.ch.isTypeIdenticalTo(info.valueType, baseInfo.valueType) { + if baseInfo != nil && b.ch.isTypeIdenticalTo(b.ch.getValueTypeOfIndexInfo(info), b.ch.getValueTypeOfIndexInfo(baseInfo)) { continue } } diff --git a/tsc/internal/checker/nodebuilderimpl.go b/tsc/internal/checker/nodebuilderimpl.go index 7c90f3f320fbc..6c1cc41faa36d 100644 --- a/tsc/internal/checker/nodebuilderimpl.go +++ b/tsc/internal/checker/nodebuilderimpl.go @@ -2226,15 +2226,18 @@ func (b *NodeBuilderImpl) indexInfoToIndexSignatureDeclarationHelper(indexInfo * indexingParameter := b.f.NewParameterDeclaration(nil, nil, b.newIdentifier(name, nil /*symbol*/), nil, indexerTypeNode, nil) if typeNode == nil { - if indexInfo.valueType == nil { + // Resolve deferred index value types (see getIndexInfosOfIndexSymbol / #64998). + // Intentionally empty index infos (no declaration and no value type) still surface as any + // and set encounteredError unless AllowEmptyIndexInfoType is set. + if indexInfo.Declaration() == nil && indexInfo.ValueType() == nil { typeNode = b.f.NewKeywordTypeNode(ast.KindAnyKeyword) + if b.ctx.flags&nodebuilder.FlagsAllowEmptyIndexInfoType == 0 { + b.ctx.encounteredError = true + } } else { - typeNode = b.typeToTypeNode(indexInfo.valueType) + typeNode = b.typeToTypeNode(b.ch.getValueTypeOfIndexInfo(indexInfo)) } } - if indexInfo.valueType == nil && b.ctx.flags&nodebuilder.FlagsAllowEmptyIndexInfoType == 0 { - b.ctx.encounteredError = true - } b.ctx.approximateLength += len(name) + 4 var modifiers *ast.ModifierList if indexInfo.isReadonly { diff --git a/tsc/internal/checker/relater.go b/tsc/internal/checker/relater.go index 562ae0451cb43..737a0bc33482f 100644 --- a/tsc/internal/checker/relater.go +++ b/tsc/internal/checker/relater.go @@ -2851,7 +2851,7 @@ func (c *Checker) getTypeOfPropertyInType(t *Type, name string) *Type { } indexInfo := c.getApplicableIndexInfoForName(t, name) if indexInfo != nil { - return indexInfo.valueType + return c.getValueTypeOfIndexInfo(indexInfo) } return c.undefinedType } @@ -4640,10 +4640,10 @@ func (r *Relater) indexSignaturesRelatedTo(source *Type, target *Type, sourceIsP for _, targetInfo := range indexInfos { var related Ternary switch { - case r.relation != r.c.strictSubtypeRelation && !sourceIsPrimitive && targetHasStringIndex && targetInfo.valueType.flags&TypeFlagsAny != 0: + case r.relation != r.c.strictSubtypeRelation && !sourceIsPrimitive && targetHasStringIndex && r.c.getValueTypeOfIndexInfo(targetInfo).flags&TypeFlagsAny != 0: related = TernaryTrue case r.c.isGenericMappedType(source) && targetHasStringIndex: - related = r.isRelatedTo(r.c.getTemplateTypeFromMappedType(source), targetInfo.valueType, RecursionFlagsBoth, reportErrors) + related = r.isRelatedTo(r.c.getTemplateTypeFromMappedType(source), r.c.getValueTypeOfIndexInfo(targetInfo), RecursionFlagsBoth, reportErrors) default: related = r.typeRelatedToIndexInfo(source, targetInfo, reportErrors, intersectionState) } @@ -4708,7 +4708,7 @@ func (r *Relater) membersRelatedToIndexInfo(source *Type, targetInfo *IndexInfo, } else { t = r.c.getTypeWithFacts(propType, TypeFactsNEUndefined) } - related := r.isRelatedToEx(t, targetInfo.valueType, RecursionFlagsBoth, reportErrors, nil /*headMessage*/, intersectionState) + related := r.isRelatedToEx(t, r.c.getValueTypeOfIndexInfo(targetInfo), RecursionFlagsBoth, reportErrors, nil /*headMessage*/, intersectionState) if related == TernaryFalse { if reportErrors { r.reportError(diagnostics.Property_0_is_incompatible_with_index_signature, r.c.symbolToString(prop)) @@ -4731,7 +4731,7 @@ func (r *Relater) membersRelatedToIndexInfo(source *Type, targetInfo *IndexInfo, } func (r *Relater) indexInfoRelatedTo(sourceInfo *IndexInfo, targetInfo *IndexInfo, reportErrors bool, intersectionState IntersectionState) Ternary { - related := r.isRelatedToEx(sourceInfo.valueType, targetInfo.valueType, RecursionFlagsBoth, reportErrors, nil /*headMessage*/, intersectionState) + related := r.isRelatedToEx(r.c.getValueTypeOfIndexInfo(sourceInfo), r.c.getValueTypeOfIndexInfo(targetInfo), RecursionFlagsBoth, reportErrors, nil /*headMessage*/, intersectionState) if related == TernaryFalse && reportErrors { if sourceInfo.keyType == targetInfo.keyType { r.reportError(diagnostics.X_0_index_signatures_are_incompatible, r.c.TypeToString(sourceInfo.keyType)) @@ -4750,7 +4750,7 @@ func (r *Relater) indexSignaturesIdenticalTo(source *Type, target *Type) Ternary } for _, targetInfo := range targetInfos { sourceInfo := r.c.getIndexInfoOfType(source, targetInfo.keyType) - if !(sourceInfo != nil && r.isRelatedTo(sourceInfo.valueType, targetInfo.valueType, RecursionFlagsBoth, false) != TernaryFalse && sourceInfo.isReadonly == targetInfo.isReadonly) { + if !(sourceInfo != nil && r.isRelatedTo(r.c.getValueTypeOfIndexInfo(sourceInfo), r.c.getValueTypeOfIndexInfo(targetInfo), RecursionFlagsBoth, false) != TernaryFalse && sourceInfo.isReadonly == targetInfo.isReadonly) { return TernaryFalse } } diff --git a/tsc/internal/checker/types.go b/tsc/internal/checker/types.go index 2af3c46a0d198..61b6217b54438 100644 --- a/tsc/internal/checker/types.go +++ b/tsc/internal/checker/types.go @@ -1786,12 +1786,15 @@ type IndexInfo struct { declaration *ast.Node // IndexSignatureDeclaration indexSymbol *ast.Symbol // Synthetic property symbol for this index signature components []*ast.Node // ElementWithComputedPropertyName + mapper *TypeMapper // Applied when lazily resolving a deferred valueType } func (info *IndexInfo) KeyType() *Type { return info.keyType } +// ValueType returns the cached value type, which may be nil when resolution was +// deferred (see Checker.getValueTypeOfIndexInfo). Prefer Checker.GetValueTypeOfIndexInfo. func (info *IndexInfo) ValueType() *Type { return info.valueType } diff --git a/tsc/internal/compiler/declaration_checkers_64998_test.go b/tsc/internal/compiler/declaration_checkers_64998_test.go new file mode 100644 index 0000000000000..3f8fb1fc8f7df --- /dev/null +++ b/tsc/internal/compiler/declaration_checkers_64998_test.go @@ -0,0 +1,78 @@ +package compiler_test + +import ( + "strings" + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/bundled" + "github.com/microsoft/TypeScript/tsc/internal/compiler" + "github.com/microsoft/TypeScript/tsc/internal/core" + "github.com/microsoft/TypeScript/tsc/internal/tsoptions" + "github.com/microsoft/TypeScript/tsc/internal/tspath" + "github.com/microsoft/TypeScript/tsc/internal/vfs/vfstest" + "gotest.tools/v3/assert" +) + +// Regression for microsoft/TypeScript#64998: declaration emit of a circular +// `Status = keyof StatusClass | ""` (StatusClass members typed as Status, plus a +// string index signature) must not collapse to `any` when status.ts and +// consumer.ts land on different checkers. +func TestDeclarationEmitCheckers64998(t *testing.T) { + t.Parallel() + + files := map[string]string{ + "/types.ts": `export type Key = string;`, + "/status.ts": `import type { Key } from "./types"; +export class StatusClass { + RUNNING: Status = "RUNNING"; + [key: string]: Status; +} +export type Status = keyof StatusClass | ""; +export const statusFromAPI: { [key: Key]: Status } = {}; +`, + "/consumer.ts": `import { statusFromAPI } from "./status"; +export const current = statusFromAPI["RUNNING"]; +`, + "/tsconfig.json": `{ + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "/out", + "rootDir": "/", + "strict": true + }, + "files": ["types.ts", "status.ts", "consumer.ts"] + }`, + } + + for _, n := range []int{1, 2, 3, 4} { + t.Run("checkers_"+string(rune('0'+n)), func(t *testing.T) { + t.Parallel() + fs := bundled.WrapFS(vfstest.FromMap(files, tspath.CaseInsensitive)) + host := compiler.NewCompilerHost(fs, bundled.LibPath(), nil, nil, nil) + checkers := n + parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile( + "/tsconfig.json", + &core.CompilerOptions{Checkers: &checkers}, + nil, + tsoptions.NewDefaultParseConfigHost(fs), + nil, + ) + assert.Equal(t, len(errors), 0) + program := compiler.NewProgram(compiler.ProgramOptions{Config: parsed, Host: host}) + + var emitted string + result := program.Emit(t.Context(), compiler.EmitOptions{ + WriteFile: func(fileName tspath.RootedFilePath, text string, data *compiler.WriteFileData) error { + if strings.HasSuffix(string(fileName), "consumer.d.ts") { + emitted = text + } + return nil + }, + }) + assert.Equal(t, len(result.Diagnostics), 0, "diags=%v", result.Diagnostics) + assert.Assert(t, strings.Contains(emitted, "Status"), "emit=%q", emitted) + assert.Assert(t, !strings.Contains(emitted, ": any"), "emit=%q", emitted) + }) + } +} From 119d74ce8d1a22cba4f6eb5f9bfce5e0f18ebc93 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 29 Oct 2026 22:03:49 +0000 Subject: [PATCH 2/2] fix(64998): track cycles when resolving deferred index value types Address PR review: do not cache the in-progress unresolved sentinel as a usable any-flagged type for recursive index signatures such as `interface I { [k: string]: I[string] }`. Resolve deferred value types via the type-resolution stack and report TS2577 on circularity. Add negative tests for direct recursion and alias-mediated recursion (index-first and alias-first), plus a control that recursive `interface Rec { [k: string]: Rec }` still succeeds. Co-authored-by: timurzvural --- tsc/internal/checker/checker.go | 37 +++++-- .../compiler/circular_index_64998_test.go | 96 +++++++++++++++++++ 2 files changed, 123 insertions(+), 10 deletions(-) create mode 100644 tsc/internal/compiler/circular_index_64998_test.go diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 62360353e1226..fc99861d782b2 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -65,6 +65,7 @@ const ( TypeSystemPropertyNameWriteType TypeSystemPropertyNameInitializerIsUndefined TypeSystemPropertyNameAliasTarget + TypeSystemPropertyNameIndexInfoValueType ) type TypeResolution struct { @@ -19272,6 +19273,8 @@ func (c *Checker) typeResolutionHasProperty(r *TypeResolution) bool { return c.valueSymbolLinks.Get(r.target.(*ast.Symbol)).getWriteType() != nil case TypeSystemPropertyNameAliasTarget: return c.aliasSymbolLinks.Get(r.target.(*ast.Symbol)).aliasTarget != nil + case TypeSystemPropertyNameIndexInfoValueType: + return r.target.(*IndexInfo).valueType != nil } panic("Unhandled case in typeResolutionHasProperty") } @@ -26033,21 +26036,35 @@ func (c *Checker) newIndexInfo(keyType *Type, valueType *Type, isReadonly bool, // getValueTypeOfIndexInfo returns the value type of an index signature, resolving it // lazily from the declaration when it was deferred by getIndexInfosOfIndexSymbol. +// Resolution is tracked with the type-resolution stack so direct recursion such as +// `interface I { [k: string]: I[string] }` reports circularity instead of caching the +// in-progress unresolved sentinel as a usable any-flagged type (#64998 review). func (c *Checker) getValueTypeOfIndexInfo(info *IndexInfo) *Type { if info.valueType != nil { return info.valueType } - if info.declaration != nil { - if returnTypeNode := info.declaration.Type(); returnTypeNode != nil { - // Assign a temporary non-nil value before resolving to avoid recursive - // re-entry through the same IndexInfo while the value type is circularly - // referenced from the type being resolved (e.g. keyof). - info.valueType = c.unresolvedType - info.valueType = c.instantiateType(c.getTypeFromTypeNode(returnTypeNode), info.mapper) - return info.valueType - } + if info.declaration == nil { + info.valueType = c.anyType + return info.valueType + } + returnTypeNode := info.declaration.Type() + if returnTypeNode == nil { + info.valueType = c.anyType + return info.valueType + } + if !c.pushTypeResolution(info, TypeSystemPropertyNameIndexInfoValueType) { + return c.errorType + } + t := c.instantiateType(c.getTypeFromTypeNode(returnTypeNode), info.mapper) + if !c.popTypeResolution() { + // Index signature value types are the annotation on the index signature + // declaration (analogous to a return type annotation). + c.error(returnTypeNode, diagnostics.Return_type_annotation_circularly_references_itself) + t = c.errorType + } + if info.valueType == nil { + info.valueType = t } - info.valueType = c.anyType return info.valueType } diff --git a/tsc/internal/compiler/circular_index_64998_test.go b/tsc/internal/compiler/circular_index_64998_test.go new file mode 100644 index 0000000000000..ce6293ba89a4f --- /dev/null +++ b/tsc/internal/compiler/circular_index_64998_test.go @@ -0,0 +1,96 @@ +package compiler_test + +import ( + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/ast" + "github.com/microsoft/TypeScript/tsc/internal/bundled" + "github.com/microsoft/TypeScript/tsc/internal/compiler" + "github.com/microsoft/TypeScript/tsc/internal/core" + "github.com/microsoft/TypeScript/tsc/internal/tsoptions" + "github.com/microsoft/TypeScript/tsc/internal/tspath" + "github.com/microsoft/TypeScript/tsc/internal/vfs/vfstest" + "gotest.tools/v3/assert" +) + +func checkCircularIndexFile(t *testing.T, src string) (typeStr string, diags []*ast.Diagnostic) { + t.Helper() + fs := bundled.WrapFS(vfstest.FromMap(map[string]string{ + "/test.ts": src, + "/tsconfig.json": `{"compilerOptions":{"strict":true},"files":["test.ts"]}`, + }, tspath.CaseInsensitive)) + host := compiler.NewCompilerHost(fs, bundled.LibPath(), nil, nil, nil) + n := 1 + parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile("/tsconfig.json", &core.CompilerOptions{Checkers: &n}, nil, tsoptions.NewDefaultParseConfigHost(fs), nil) + assert.Equal(t, len(errors), 0) + program := compiler.NewProgram(compiler.ProgramOptions{Config: parsed, Host: host}) + program.BindSourceFiles() + file := program.GetSourceFile("/test.ts") + c, done := program.GetTypeCheckerForFileExclusive(t.Context(), file) + defer done() + // Force-resolve T (and thus deferred index value types) before collecting diagnostics. + for _, stmt := range file.Statements.Nodes { + if stmt.Kind == ast.KindTypeAliasDeclaration && stmt.Name() != nil && stmt.Name().Text() == "T" { + typeStr = c.TypeToString(c.GetTypeAtLocation(stmt.Name())) + } + } + diags = c.GetDiagnostics(t.Context(), file) + return +} + +func hasDiagnosticCode(diags []*ast.Diagnostic, code int32) bool { + for _, d := range diags { + if d.Code() == code { + return true + } + } + return false +} + +// Negative regressions for #64998 review: recursive index-signature value types +// must report circularity (TS2577) instead of silently becoming any via an +// in-progress unresolved sentinel. Covers direct recursion and recursion through +// a type alias, with both index-first and alias-first declaration order. +func TestCircularIndexValueTypeDiagnostics64998(t *testing.T) { + t.Parallel() + + const ts2577 = int32(2577) // Return_type_annotation_circularly_references_itself + const ts2456 = int32(2456) // Type_alias_0_circularly_references_itself + + t.Run("direct_indexed", func(t *testing.T) { + t.Parallel() + _, diags := checkCircularIndexFile(t, `interface I { [k: string]: I[string] } +export type T = I[string];`) + assert.Assert(t, hasDiagnosticCode(diags, ts2577), "expected TS2577, diags=%v", diags) + }) + + t.Run("alias_first", func(t *testing.T) { + t.Parallel() + // Type alias resolved before the interface's index infos. + _, diags := checkCircularIndexFile(t, `type Alias = Box[string]; +interface Box { [k: string]: Alias } +export type T = Alias;`) + assert.Assert(t, hasDiagnosticCode(diags, ts2577) || hasDiagnosticCode(diags, ts2456), + "expected TS2577 or TS2456, diags=%v", diags) + }) + + t.Run("index_first", func(t *testing.T) { + t.Parallel() + // Interface appears first so index infos are built before the alias. + _, diags := checkCircularIndexFile(t, `interface Box { [k: string]: Alias } +type Alias = Box[string]; +export type T = Alias;`) + assert.Assert(t, hasDiagnosticCode(diags, ts2577) || hasDiagnosticCode(diags, ts2456), + "expected TS2577 or TS2456, diags=%v", diags) + }) + + t.Run("recursive_interface_ok", func(t *testing.T) { + t.Parallel() + // Referring to the interface itself (not through an indexed access) is a + // normal recursive type and must not error. + typ, diags := checkCircularIndexFile(t, `interface Rec { [k: string]: Rec } +export type T = Rec[string];`) + assert.Equal(t, typ, "Rec") + assert.Equal(t, len(diags), 0, "diags=%v", diags) + }) +}