Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions _packages/native-preview/src/enums/objectFlags.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export enum ObjectFlags {
IdenticalBaseTypeCalculated = 1 << 27,
IdenticalBaseTypeExists = 1 << 28,
UnresolvedMembers = 1 << 29,
FromTypeNode = 1 << 30,
IsGenericTypeComputed = 1 << 22,
IsGenericObjectType = 1 << 23,
IsGenericIndexType = 1 << 24,
Expand Down
1 change: 1 addition & 0 deletions _packages/native-preview/src/enums/objectFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export var ObjectFlags: any;
ObjectFlags[ObjectFlags["IdenticalBaseTypeCalculated"] = 134217728] = "IdenticalBaseTypeCalculated";
ObjectFlags[ObjectFlags["IdenticalBaseTypeExists"] = 268435456] = "IdenticalBaseTypeExists";
ObjectFlags[ObjectFlags["UnresolvedMembers"] = 536870912] = "UnresolvedMembers";
ObjectFlags[ObjectFlags["FromTypeNode"] = 1073741824] = "FromTypeNode";
ObjectFlags[ObjectFlags["IsGenericTypeComputed"] = 4194304] = "IsGenericTypeComputed";
ObjectFlags[ObjectFlags["IsGenericObjectType"] = 8388608] = "IsGenericObjectType";
ObjectFlags[ObjectFlags["IsGenericIndexType"] = 16777216] = "IsGenericIndexType";
Expand Down
27 changes: 19 additions & 8 deletions internal/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -22741,7 +22741,7 @@ func (c *Checker) getTypeFromClassOrInterfaceReference(node *ast.Node, symbol *a
// of the class or interface.
localTypeArguments := c.fillMissingTypeArguments(c.getTypeArgumentsFromNode(node), typeParameters, minTypeArgumentCount, isJs)
typeArguments := append(d.OuterTypeParameters(), localTypeArguments...)
return c.createTypeReference(t, typeArguments)
return c.createTypeReferenceEx(t, typeArguments, ObjectFlagsFromTypeNode)
}
if c.checkNoTypeArguments(node, symbol) {
return t
Expand Down Expand Up @@ -22836,11 +22836,11 @@ func (c *Checker) createNormalizedTypeReference(target *Type, typeArguments []*T
return c.createTypeReference(target, typeArguments)
}

func (c *Checker) createNormalizedTupleType(target *Type, elementTypes []*Type) *Type {
func (c *Checker) createNormalizedTupleTypeEx(target *Type, elementTypes []*Type, objectFlags ObjectFlags) *Type {
d := target.AsTupleType()
if d.combinedFlags&ElementFlagsNonRequired == 0 {
// No need to normalize when we only have regular required elements
return c.createTypeReference(target, elementTypes)
return c.createTypeReferenceEx(target, elementTypes, objectFlags)
}
if d.combinedFlags&ElementFlagsVariadic != 0 {
for i, e := range elementTypes {
Expand All @@ -22854,7 +22854,7 @@ func (c *Checker) createNormalizedTupleType(target *Type, elementTypes []*Type)
})
if c.checkCrossProductUnion(checkTypes) {
return c.mapType(e, func(t *Type) *Type {
return c.createNormalizedTupleType(target, core.ReplaceElement(elementTypes, i, t))
return c.createNormalizedTupleTypeEx(target, core.ReplaceElement(elementTypes, i, t), objectFlags)
})
}
}
Expand All @@ -22879,11 +22879,15 @@ func (c *Checker) createNormalizedTupleType(target *Type, elementTypes []*Type)
case tupleTarget == c.emptyGenericType:
return c.emptyObjectType
case len(n.types) != 0:
return c.createTypeReference(tupleTarget, n.types)
return c.createTypeReferenceEx(tupleTarget, n.types, objectFlags)
}
return tupleTarget
}

func (c *Checker) createNormalizedTupleType(target *Type, elementTypes []*Type) *Type {
return c.createNormalizedTupleTypeEx(target, elementTypes, ObjectFlagsNone)
}

type TupleNormalizer struct {
c *Checker
types []*Type
Expand Down Expand Up @@ -23662,7 +23666,11 @@ func (c *Checker) getTypeFromArrayOrTupleTypeNode(node *ast.Node) *Type {
} else {
elementTypes = core.Map(node.Elements(), c.getTypeFromTypeNode)
}
links.resolvedType = c.createNormalizedTypeReference(target, elementTypes)
if target.objectFlags&ObjectFlagsTuple != 0 {
links.resolvedType = c.createNormalizedTupleTypeEx(target, elementTypes, ObjectFlagsFromTypeNode)
} else {
links.resolvedType = c.createTypeReferenceEx(target, elementTypes, ObjectFlagsFromTypeNode)
}
}
}
return links.resolvedType
Expand Down Expand Up @@ -24625,13 +24633,16 @@ func (c *Checker) tryCreateTypeReference(target *Type, typeArguments []*Type) *T
}

func (c *Checker) createTypeReference(target *Type, typeArguments []*Type) *Type {
return c.createTypeReferenceEx(target, typeArguments, ObjectFlagsNone)
}

func (c *Checker) createTypeReferenceEx(target *Type, typeArguments []*Type, objectFlags ObjectFlags) *Type {
Comment on lines 24640 to +24645
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createTypeReferenceEx caches instantiations only by typeArguments (getTypeListKey). That ignores the objectFlags parameter, so a previously-cached instantiation without ObjectFlagsFromTypeNode can be returned to callers that request it (and vice versa). This makes the new recursion-identity behavior depend on call order and can cause both missed recursion detection and missed errors. Consider including the relevant objectFlags bits (at least ObjectFlagsFromTypeNode) in the cache key and/or maintaining a separate instantiation cache for FromTypeNode references so the flag is applied deterministically only where intended.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

@ahejlsberg ahejlsberg Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good observation and indeed something that was considered. The issue with tracking the flag in the cache key is that it creates distinct object identities for effectively identical types, which we then have to reconcile in multiple places elsewhere, for example in order to avoid strange types like number[] | number[] (two types that differ only by the flag). The flag is only used by a heuristic that ultimately also depends on type IDs (which similarly depend on declaration order), so it is a reasonable compromise to just record it in the first type instantiation.

id := getTypeListKey(typeArguments)
intf := target.AsInterfaceType()
if t, ok := intf.instantiations[id]; ok {
return t
}
t := c.newObjectType(ObjectFlagsReference, target.symbol)
t.objectFlags |= c.getPropagatingFlagsOfTypes(typeArguments, TypeFlagsNone)
t := c.newObjectType(ObjectFlagsReference|objectFlags|c.getPropagatingFlagsOfTypes(typeArguments, TypeFlagsNone), target.symbol)
d := t.AsTypeReference()
d.target = target
d.resolvedTypeArguments = typeArguments
Expand Down
8 changes: 5 additions & 3 deletions internal/checker/relater.go
Original file line number Diff line number Diff line change
Expand Up @@ -839,12 +839,14 @@ func getRecursionIdentity(t *Type) RecursionId {
// unique AST node.
return asRecursionId(t.AsTypeReference().node)
}
if t.symbol != nil && !(t.objectFlags&ObjectFlagsAnonymous != 0 && t.symbol.Flags&ast.SymbolFlagsClass != 0) {
if t.symbol != nil && !(t.objectFlags&ObjectFlagsAnonymous != 0 && t.symbol.Flags&ast.SymbolFlagsClass != 0) && t.objectFlags&ObjectFlagsFromTypeNode == 0 {
// We track object types that have a symbol by that symbol (representing the origin of the type), but
// exclude the static side of a class since it shares its symbol with the instance side.
// exclude the static sides of classes (since they share their symbols with the instance sides) and type
// references that originate in resolution of AST type nodes (since such type nodes cannot be the source
// of generative recursion without first being instantiated).
Comment on lines +842 to +846
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change adjusts recursion identity handling to fix a checker soundness gap, but the PR doesn't add a regression test for #3426 (nested arrays from distinct namespaces should now produce TS2322). Please add a minimal compiler test case under testdata/tests/cases/compiler/ that fails before this change and passes after, so we don’t regress this behavior again.

Copilot generated this review using guidance from repository custom instructions.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now fixed.

return asRecursionId(t.symbol)
}
if isTupleType(t) {
if isTupleType(t) && t.objectFlags&ObjectFlagsFromTypeNode == 0 {
return asRecursionId(t.Target())
}
}
Expand Down
1 change: 1 addition & 0 deletions internal/checker/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,7 @@ const (
ObjectFlagsIdenticalBaseTypeCalculated = 1 << 27 // has had `getSingleBaseForNonAugmentingSubtype` invoked on it already
ObjectFlagsIdenticalBaseTypeExists = 1 << 28 // has a defined cachedEquivalentBaseType member
ObjectFlagsUnresolvedMembers = 1 << 29 // Member resolution in process
ObjectFlagsFromTypeNode = 1 << 30 // Originates in resolution of AST type node
// Flags that require TypeFlags.UnionOrIntersection or TypeFlags.Substitution
ObjectFlagsIsGenericTypeComputed = 1 << 22 // IsGenericObjectType flag has been computed
ObjectFlagsIsGenericObjectType = 1 << 23 // Union or intersection contains generic object type
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
deeplyNestedArrayTypes.ts(34,5): error TS2322: Type 'B.Outer' is not assignable to type 'A.Outer'.
Types of property 'inners' are incompatible.
Type 'B.Inner[]' is not assignable to type 'A.Inner[]'.
Type 'B.Inner' is not assignable to type 'A.Inner'.
Types of property 'mids' are incompatible.
Type 'B.Mid[]' is not assignable to type 'A.Mid[]'.
Type 'B.Mid' is not assignable to type 'A.Mid'.
Types of property 'leaves' are incompatible.
Type 'B.Leaf[]' is not assignable to type 'A.Leaf[]'.
Type 'B.Leaf' is not assignable to type 'A.Leaf'.
Types of property 'id' are incompatible.
Type 'number' is not assignable to type 'string'.


==== deeplyNestedArrayTypes.ts (1 errors) ====
// https://github.com/microsoft/typescript-go/issues/3426

namespace A {
export type Outer = {
inners: Inner[];
}
export type Inner = {
mids: Mid[];
}
export type Mid = {
leaves: Leaf[];
}
export type Leaf = {
id: string;
}
}

namespace B {
export type Outer = {
inners: Inner[];
}
export type Inner = {
mids: Mid[];
}
export type Mid = {
leaves: Leaf[];
}
export type Leaf = {
id: number;
}
}

function test(a: A.Outer, b: B.Outer) {
a = b
~
!!! error TS2322: Type 'B.Outer' is not assignable to type 'A.Outer'.
!!! error TS2322: Types of property 'inners' are incompatible.
!!! error TS2322: Type 'B.Inner[]' is not assignable to type 'A.Inner[]'.
!!! error TS2322: Type 'B.Inner' is not assignable to type 'A.Inner'.
!!! error TS2322: Types of property 'mids' are incompatible.
!!! error TS2322: Type 'B.Mid[]' is not assignable to type 'A.Mid[]'.
!!! error TS2322: Type 'B.Mid' is not assignable to type 'A.Mid'.
!!! error TS2322: Types of property 'leaves' are incompatible.
!!! error TS2322: Type 'B.Leaf[]' is not assignable to type 'A.Leaf[]'.
!!! error TS2322: Type 'B.Leaf' is not assignable to type 'A.Leaf'.
!!! error TS2322: Types of property 'id' are incompatible.
!!! error TS2322: Type 'number' is not assignable to type 'string'.
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//// [tests/cases/compiler/deeplyNestedArrayTypes.ts] ////

=== deeplyNestedArrayTypes.ts ===
// https://github.com/microsoft/typescript-go/issues/3426

namespace A {
>A : Symbol(A, Decl(deeplyNestedArrayTypes.ts, 0, 0))

export type Outer = {
>Outer : Symbol(Outer, Decl(deeplyNestedArrayTypes.ts, 2, 13))

inners: Inner[];
>inners : Symbol(inners, Decl(deeplyNestedArrayTypes.ts, 3, 25))
>Inner : Symbol(Inner, Decl(deeplyNestedArrayTypes.ts, 5, 5))
}
export type Inner = {
>Inner : Symbol(Inner, Decl(deeplyNestedArrayTypes.ts, 5, 5))

mids: Mid[];
>mids : Symbol(mids, Decl(deeplyNestedArrayTypes.ts, 6, 25))
>Mid : Symbol(Mid, Decl(deeplyNestedArrayTypes.ts, 8, 5))
}
export type Mid = {
>Mid : Symbol(Mid, Decl(deeplyNestedArrayTypes.ts, 8, 5))

leaves: Leaf[];
>leaves : Symbol(leaves, Decl(deeplyNestedArrayTypes.ts, 9, 23))
>Leaf : Symbol(Leaf, Decl(deeplyNestedArrayTypes.ts, 11, 5))
}
export type Leaf = {
>Leaf : Symbol(Leaf, Decl(deeplyNestedArrayTypes.ts, 11, 5))

id: string;
>id : Symbol(id, Decl(deeplyNestedArrayTypes.ts, 12, 24))
}
}

namespace B {
>B : Symbol(B, Decl(deeplyNestedArrayTypes.ts, 15, 1))

export type Outer = {
>Outer : Symbol(Outer, Decl(deeplyNestedArrayTypes.ts, 17, 13))

inners: Inner[];
>inners : Symbol(inners, Decl(deeplyNestedArrayTypes.ts, 18, 25))
>Inner : Symbol(Inner, Decl(deeplyNestedArrayTypes.ts, 20, 5))
}
export type Inner = {
>Inner : Symbol(Inner, Decl(deeplyNestedArrayTypes.ts, 20, 5))

mids: Mid[];
>mids : Symbol(mids, Decl(deeplyNestedArrayTypes.ts, 21, 25))
>Mid : Symbol(Mid, Decl(deeplyNestedArrayTypes.ts, 23, 5))
}
export type Mid = {
>Mid : Symbol(Mid, Decl(deeplyNestedArrayTypes.ts, 23, 5))

leaves: Leaf[];
>leaves : Symbol(leaves, Decl(deeplyNestedArrayTypes.ts, 24, 23))
>Leaf : Symbol(Leaf, Decl(deeplyNestedArrayTypes.ts, 26, 5))
}
export type Leaf = {
>Leaf : Symbol(Leaf, Decl(deeplyNestedArrayTypes.ts, 26, 5))

id: number;
>id : Symbol(id, Decl(deeplyNestedArrayTypes.ts, 27, 24))
}
}

function test(a: A.Outer, b: B.Outer) {
>test : Symbol(test, Decl(deeplyNestedArrayTypes.ts, 30, 1))
>a : Symbol(a, Decl(deeplyNestedArrayTypes.ts, 32, 14))
>A : Symbol(A, Decl(deeplyNestedArrayTypes.ts, 0, 0))
>Outer : Symbol(A.Outer, Decl(deeplyNestedArrayTypes.ts, 2, 13))
>b : Symbol(b, Decl(deeplyNestedArrayTypes.ts, 32, 25))
>B : Symbol(B, Decl(deeplyNestedArrayTypes.ts, 15, 1))
>Outer : Symbol(B.Outer, Decl(deeplyNestedArrayTypes.ts, 17, 13))

a = b
>a : Symbol(a, Decl(deeplyNestedArrayTypes.ts, 32, 14))
>b : Symbol(b, Decl(deeplyNestedArrayTypes.ts, 32, 25))
}

72 changes: 72 additions & 0 deletions testdata/baselines/reference/compiler/deeplyNestedArrayTypes.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//// [tests/cases/compiler/deeplyNestedArrayTypes.ts] ////

=== deeplyNestedArrayTypes.ts ===
// https://github.com/microsoft/typescript-go/issues/3426

namespace A {
export type Outer = {
>Outer : Outer

inners: Inner[];
>inners : Inner[]
}
export type Inner = {
>Inner : Inner

mids: Mid[];
>mids : Mid[]
}
export type Mid = {
>Mid : Mid

leaves: Leaf[];
>leaves : Leaf[]
}
export type Leaf = {
>Leaf : Leaf

id: string;
>id : string
}
}

namespace B {
export type Outer = {
>Outer : Outer

inners: Inner[];
>inners : Inner[]
}
export type Inner = {
>Inner : Inner

mids: Mid[];
>mids : Mid[]
}
export type Mid = {
>Mid : Mid

leaves: Leaf[];
>leaves : Leaf[]
}
export type Leaf = {
>Leaf : Leaf

id: number;
>id : number
}
}

function test(a: A.Outer, b: B.Outer) {
>test : (a: A.Outer, b: B.Outer) => void
>a : A.Outer
>A : any
>b : B.Outer
>B : any

a = b
>a = b : B.Outer
>a : A.Outer
>b : B.Outer
}

Loading
Loading