From 399708cd38267b18da738521754fb4edbf49fe4e Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Sun, 18 Jan 2026 10:15:38 -1000 Subject: [PATCH 1/2] Fix stack overflow caused by circular destructuring --- internal/checker/checker.go | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/internal/checker/checker.go b/internal/checker/checker.go index 8299b82686..409a7d08ac 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -13326,6 +13326,7 @@ func (c *Checker) getNarrowedTypeOfSymbol(symbol *ast.Symbol, location *ast.Node t := c.getTypeOfSymbol(symbol) declaration := symbol.ValueDeclaration if declaration != nil { + switch { // If we have a non-rest binding element with no initializer declared as a const variable or a const-like // parameter (a parameter for which there are no assignments in the function body), and if the parent type // for the destructuring is a union type, one or more of the binding elements may represent discriminant @@ -13349,7 +13350,7 @@ func (c *Checker) getNarrowedTypeOfSymbol(symbol *ast.Symbol, location *ast.Node // the binding pattern AST instance for '{ kind, payload }' as a pseudo-reference and narrow this reference // as if it occurred in the specified location. We then recompute the narrowed binding element type by // destructuring from the narrowed parent type. - if ast.IsBindingElement(declaration) && declaration.Initializer() == nil && !hasDotDotDotToken(declaration) && len(declaration.Parent.Elements()) >= 2 { + case ast.IsBindingElement(declaration) && declaration.Initializer() == nil && !hasDotDotDotToken(declaration) && len(declaration.Parent.Elements()) >= 2: parent := declaration.Parent.Parent rootDeclaration := ast.GetRootDeclaration(parent) if ast.IsVariableDeclaration(rootDeclaration) && c.getCombinedNodeFlagsCached(rootDeclaration)&ast.NodeFlagsConstant != 0 || ast.IsParameter(rootDeclaration) { @@ -13361,21 +13362,21 @@ func (c *Checker) getNarrowedTypeOfSymbol(symbol *ast.Symbol, location *ast.Node if parentType != nil { parentTypeConstraint = c.mapType(parentType, c.getBaseConstraintOrType) } - links.flags &^= NodeCheckFlagsInCheckIdentifier if parentTypeConstraint != nil && parentTypeConstraint.flags&TypeFlagsUnion != 0 && !(ast.IsParameter(rootDeclaration) && c.isSomeSymbolAssigned(rootDeclaration)) { pattern := declaration.Parent narrowedType := c.getFlowTypeOfReferenceEx(pattern, parentTypeConstraint, parentTypeConstraint, nil /*flowContainer*/, getFlowNodeOfNode(location)) if narrowedType.flags&TypeFlagsNever != 0 { - return c.neverType + t = c.neverType + } else { + // Destructurings are validated against the parent type elsewhere. Here we disable tuple bounds + // checks because the narrowed type may have lower arity than the full parent type. For example, + // for the declaration [x, y]: [1, 2] | [3], we may have narrowed the parent type to just [3]. + t = c.getBindingElementTypeFromParentType(declaration, narrowedType, true /*noTupleBoundsCheck*/) } - // Destructurings are validated against the parent type elsewhere. Here we disable tuple bounds - // checks because the narrowed type may have lower arity than the full parent type. For example, - // for the declaration [x, y]: [1, 2] | [3], we may have narrowed the parent type to just [3]. - return c.getBindingElementTypeFromParentType(declaration, narrowedType, true /*noTupleBoundsCheck*/) } + links.flags &^= NodeCheckFlagsInCheckIdentifier } } - } // If we have a const-like parameter with no type annotation or initializer, and if the parameter is contextually // typed by a signature with a single rest parameter of a union of tuple types, one or more of the parameters may // represent discriminant tuple elements, and we want the effects of conditional checks on such discriminants to @@ -13396,7 +13397,7 @@ func (c *Checker) getNarrowedTypeOfSymbol(symbol *ast.Symbol, location *ast.Node // the arrow function AST node for '(kind, payload) => ...' as a pseudo-reference and narrow this reference as // if it occurred in the specified location. We then recompute the narrowed parameter type by indexing into the // narrowed tuple type. - if ast.IsParameter(declaration) && declaration.Type() == nil && declaration.Initializer() == nil && !hasDotDotDotToken(declaration) { + case ast.IsParameter(declaration) && declaration.Type() == nil && declaration.Initializer() == nil && !hasDotDotDotToken(declaration): fn := declaration.Parent if len(fn.Parameters()) >= 2 && c.isContextSensitiveFunctionOrObjectLiteralMethod(fn) { contextualSignature := c.getContextualSignature(fn) @@ -13410,7 +13411,7 @@ func (c *Checker) getNarrowedTypeOfSymbol(symbol *ast.Symbol, location *ast.Node if restType.flags&TypeFlagsUnion != 0 && everyType(restType, isTupleType) && !core.Some(fn.Parameters(), c.isSomeSymbolAssigned) { narrowedType := c.getFlowTypeOfReferenceEx(fn, restType, restType, nil /*flowContainer*/, getFlowNodeOfNode(location)) index := slices.Index(fn.Parameters(), declaration) - (core.IfElse(ast.GetThisParameter(fn) != nil, 1, 0)) - return c.getIndexedAccessType(narrowedType, c.getNumberLiteralType(jsnum.Number(index))) + t = c.getIndexedAccessType(narrowedType, c.getNumberLiteralType(jsnum.Number(index))) } } } From c849bc97ac2f46a5c1275794e944e5efbff1b027 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Sun, 18 Jan 2026 10:15:48 -1000 Subject: [PATCH 2/2] Add regression test --- .../compiler/circularDestructuring.errors.txt | 21 +++++++++++++++++++ .../compiler/circularDestructuring.symbols | 9 ++++++++ .../compiler/circularDestructuring.types | 11 ++++++++++ .../cases/compiler/circularDestructuring.ts | 3 +++ 4 files changed, 44 insertions(+) create mode 100644 testdata/baselines/reference/compiler/circularDestructuring.errors.txt create mode 100644 testdata/baselines/reference/compiler/circularDestructuring.symbols create mode 100644 testdata/baselines/reference/compiler/circularDestructuring.types create mode 100644 testdata/tests/cases/compiler/circularDestructuring.ts diff --git a/testdata/baselines/reference/compiler/circularDestructuring.errors.txt b/testdata/baselines/reference/compiler/circularDestructuring.errors.txt new file mode 100644 index 0000000000..78eb35bc46 --- /dev/null +++ b/testdata/baselines/reference/compiler/circularDestructuring.errors.txt @@ -0,0 +1,21 @@ +circularDestructuring.ts(1,7): error TS2322: Type '{ c: number; f: any; }' is not assignable to type 'string | number'. +circularDestructuring.ts(1,9): error TS2339: Property 'c' does not exist on type 'string | number'. +circularDestructuring.ts(1,12): error TS2339: Property 'f' does not exist on type 'string | number'. +circularDestructuring.ts(1,12): error TS7022: 'f' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer. +circularDestructuring.ts(1,43): error TS2448: Block-scoped variable 'f' used before its declaration. + + +==== circularDestructuring.ts (5 errors) ==== + const { c, f }: string | number = { c: 0, f }; + ~~~~~~~~ +!!! error TS2322: Type '{ c: number; f: any; }' is not assignable to type 'string | number'. + ~ +!!! error TS2339: Property 'c' does not exist on type 'string | number'. + ~ +!!! error TS2339: Property 'f' does not exist on type 'string | number'. + ~ +!!! error TS7022: 'f' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer. + ~ +!!! error TS2448: Block-scoped variable 'f' used before its declaration. +!!! related TS2728 circularDestructuring.ts:1:12: 'f' is declared here. + \ No newline at end of file diff --git a/testdata/baselines/reference/compiler/circularDestructuring.symbols b/testdata/baselines/reference/compiler/circularDestructuring.symbols new file mode 100644 index 0000000000..ae5676347b --- /dev/null +++ b/testdata/baselines/reference/compiler/circularDestructuring.symbols @@ -0,0 +1,9 @@ +//// [tests/cases/compiler/circularDestructuring.ts] //// + +=== circularDestructuring.ts === +const { c, f }: string | number = { c: 0, f }; +>c : Symbol(c, Decl(circularDestructuring.ts, 0, 7)) +>f : Symbol(f, Decl(circularDestructuring.ts, 0, 10)) +>c : Symbol(c, Decl(circularDestructuring.ts, 0, 35)) +>f : Symbol(f, Decl(circularDestructuring.ts, 0, 41)) + diff --git a/testdata/baselines/reference/compiler/circularDestructuring.types b/testdata/baselines/reference/compiler/circularDestructuring.types new file mode 100644 index 0000000000..b28e2c9e4a --- /dev/null +++ b/testdata/baselines/reference/compiler/circularDestructuring.types @@ -0,0 +1,11 @@ +//// [tests/cases/compiler/circularDestructuring.ts] //// + +=== circularDestructuring.ts === +const { c, f }: string | number = { c: 0, f }; +>c : any +>f : any +>{ c: 0, f } : { c: number; f: any; } +>c : number +>0 : 0 +>f : any + diff --git a/testdata/tests/cases/compiler/circularDestructuring.ts b/testdata/tests/cases/compiler/circularDestructuring.ts new file mode 100644 index 0000000000..b9ccea3be2 --- /dev/null +++ b/testdata/tests/cases/compiler/circularDestructuring.ts @@ -0,0 +1,3 @@ +// @strict: true +// @noEmit: true +const { c, f }: string | number = { c: 0, f };