From a8cb8fc4adc9685edaa498a476c6adafd7673919 Mon Sep 17 00:00:00 2001 From: Yiin Date: Sun, 12 Apr 2026 18:14:00 +0300 Subject: [PATCH 1/3] Fix inference from unions of arrays with different nesting depths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When inferring T from a parameter typed T[] | T[][], the closely-matched inference pass cross-matches all Array constituents regardless of nesting depth. Because CompareTypes sorts union constituents by type flags (Object < Union), source types like Value[][] sort before Value[] when Value is a union or non-reference object type. The first cross-match (Value[][] to T[]) produces the wrong candidate T = Value[], and findLeftmostType picks it. Add an intermediate "deeply matched" pass between the existing isTypeOrBaseIdenticalTo and isTypeCloselyMatchedBy passes. This pass matches reference types only when their type arguments have compatible nesting structure — if a source arg is a nested reference to the same outer type (e.g., Array> inside Array), the target arg must also be nested, and vice versa. Same-depth pairs are inferred first, preventing the incorrect cross-match. Fixes #1789, fixes #3370. --- internal/checker/inference.go | 37 +++++++- .../compiler/inferenceUnionArrayDepth.js | 52 ++++++++++++ .../compiler/inferenceUnionArrayDepth.symbols | 84 +++++++++++++++++++ .../compiler/inferenceUnionArrayDepth.types | 78 +++++++++++++++++ .../compiler/inferenceUnionArrayDepth.ts | 31 +++++++ 5 files changed, 281 insertions(+), 1 deletion(-) create mode 100644 testdata/baselines/reference/compiler/inferenceUnionArrayDepth.js create mode 100644 testdata/baselines/reference/compiler/inferenceUnionArrayDepth.symbols create mode 100644 testdata/baselines/reference/compiler/inferenceUnionArrayDepth.types create mode 100644 testdata/tests/cases/compiler/inferenceUnionArrayDepth.ts diff --git a/internal/checker/inference.go b/internal/checker/inference.go index 0fc55a821ae..8729d7ddcc5 100644 --- a/internal/checker/inference.go +++ b/internal/checker/inference.go @@ -102,10 +102,15 @@ func (c *Checker) inferFromTypes(n *InferenceState, source *Type, target *Type) // First, infer between identically matching source and target constituents and remove the // matching types. tempSources, tempTargets := c.inferFromMatchingTypes(n, source.Distributed(), target.Distributed(), (*Checker).isTypeOrBaseIdenticalTo) + // Next, infer between deeply matching source and target constituents and remove the + // matching types. Types deeply match when they are instantiations of the same object + // type AND their type arguments are themselves closely matched. This prevents incorrect + // cross-matching of types like Array with Array in unions like T[] | T[][]. + tempSources2, tempTargets2 := c.inferFromMatchingTypes(n, tempSources, tempTargets, (*Checker).isTypeDeeplyMatchedBy) // Next, infer between closely matching source and target constituents and remove // the matching types. Types closely match when they are instantiations of the same // object type or instantiations of the same type alias. - sources, targets := c.inferFromMatchingTypes(n, tempSources, tempTargets, (*Checker).isTypeCloselyMatchedBy) + sources, targets := c.inferFromMatchingTypes(n, tempSources2, tempTargets2, (*Checker).isTypeCloselyMatchedBy) if len(targets) == 0 { return } @@ -1151,6 +1156,35 @@ func (c *Checker) isTypeCloselyMatchedBy(s *Type, t *Type) bool { s.alias != nil && t.alias != nil && len(s.alias.typeArguments) != 0 && s.alias.symbol == t.alias.symbol } +// isTypeDeeplyMatchedBy checks that two types are closely matched AND that their type arguments +// have compatible nesting depth. This provides a more precise matching than isTypeCloselyMatchedBy +// for cases like T[] | T[][] where all constituents share the same symbol (Array) but differ in +// nesting depth — preventing cross-matching of Array with Array>. +func (c *Checker) isTypeDeeplyMatchedBy(s *Type, t *Type) bool { + if !c.isTypeCloselyMatchedBy(s, t) { + return false + } + if s.objectFlags&ObjectFlagsReference != 0 && t.objectFlags&ObjectFlagsReference != 0 { + sr := s.AsTypeReference() + tr := t.AsTypeReference() + sArgs := sr.resolvedTypeArguments + tArgs := tr.resolvedTypeArguments + if len(sArgs) == len(tArgs) { + for i := range sArgs { + // Check that type arguments have matching nesting structure: if the source arg + // is a nested reference to the same outer type (e.g., Array>), the + // target arg must also be nested, and vice versa. + saIsNestedRef := sArgs[i].objectFlags&ObjectFlagsReference != 0 && sArgs[i].AsTypeReference().target == sr.target + taIsNestedRef := tArgs[i].objectFlags&ObjectFlagsReference != 0 && tArgs[i].AsTypeReference().target == tr.target + if saIsNestedRef != taIsNestedRef { + return false + } + } + } + } + return true +} + // Create an object with properties named in the string literal type. Every property has type `any`. func (c *Checker) createEmptyObjectTypeFromStringLiteral(t *Type) *Type { members := make(ast.SymbolTable) @@ -1608,3 +1642,4 @@ func (c *Checker) mergeInferences(target []*InferenceInfo, source []*InferenceIn } } } + diff --git a/testdata/baselines/reference/compiler/inferenceUnionArrayDepth.js b/testdata/baselines/reference/compiler/inferenceUnionArrayDepth.js new file mode 100644 index 00000000000..270c46fe3e8 --- /dev/null +++ b/testdata/baselines/reference/compiler/inferenceUnionArrayDepth.js @@ -0,0 +1,52 @@ +//// [tests/cases/compiler/inferenceUnionArrayDepth.ts] //// + +//// [inferenceUnionArrayDepth.ts] +// Regression test for https://github.com/microsoft/typescript-go/issues/1789 +// and https://github.com/microsoft/typescript-go/issues/3370 +// Inference should correctly infer T from T[] | T[][] union parameters. + +declare function flat(args: T[] | T[][]): T; + +// Case 1: Union type (issue #1789) +type Value = 1 | 2; +declare const n: Value[] | Value[][]; +const result1: Value = flat(n); // Should infer T = Value, not T = Value[] + +// Case 2: Object type (issue #3370) +type TG = { a: string }; + +function isNestedArray(arr: T[] | T[][]): arr is T[][] { + return Array.isArray(arr) && Array.isArray(arr[0]); +} + +function convert(controls: TG[] | TG[][]): TG[][] { + if (isNestedArray(controls)) { + return controls; + } else { + return [controls]; + } +} + +// Case 3: Primitive type (should already work) +declare const s: string[] | string[][]; +const result3: string = flat(s); + + +//// [inferenceUnionArrayDepth.js] +"use strict"; +// Regression test for https://github.com/microsoft/typescript-go/issues/1789 +// and https://github.com/microsoft/typescript-go/issues/3370 +// Inference should correctly infer T from T[] | T[][] union parameters. +const result1 = flat(n); // Should infer T = Value, not T = Value[] +function isNestedArray(arr) { + return Array.isArray(arr) && Array.isArray(arr[0]); +} +function convert(controls) { + if (isNestedArray(controls)) { + return controls; + } + else { + return [controls]; + } +} +const result3 = flat(s); diff --git a/testdata/baselines/reference/compiler/inferenceUnionArrayDepth.symbols b/testdata/baselines/reference/compiler/inferenceUnionArrayDepth.symbols new file mode 100644 index 00000000000..e69e5061230 --- /dev/null +++ b/testdata/baselines/reference/compiler/inferenceUnionArrayDepth.symbols @@ -0,0 +1,84 @@ +//// [tests/cases/compiler/inferenceUnionArrayDepth.ts] //// + +=== inferenceUnionArrayDepth.ts === +// Regression test for https://github.com/microsoft/typescript-go/issues/1789 +// and https://github.com/microsoft/typescript-go/issues/3370 +// Inference should correctly infer T from T[] | T[][] union parameters. + +declare function flat(args: T[] | T[][]): T; +>flat : Symbol(flat, Decl(inferenceUnionArrayDepth.ts, 0, 0)) +>T : Symbol(T, Decl(inferenceUnionArrayDepth.ts, 4, 22)) +>args : Symbol(args, Decl(inferenceUnionArrayDepth.ts, 4, 25)) +>T : Symbol(T, Decl(inferenceUnionArrayDepth.ts, 4, 22)) +>T : Symbol(T, Decl(inferenceUnionArrayDepth.ts, 4, 22)) +>T : Symbol(T, Decl(inferenceUnionArrayDepth.ts, 4, 22)) + +// Case 1: Union type (issue #1789) +type Value = 1 | 2; +>Value : Symbol(Value, Decl(inferenceUnionArrayDepth.ts, 4, 47)) + +declare const n: Value[] | Value[][]; +>n : Symbol(n, Decl(inferenceUnionArrayDepth.ts, 8, 13)) +>Value : Symbol(Value, Decl(inferenceUnionArrayDepth.ts, 4, 47)) +>Value : Symbol(Value, Decl(inferenceUnionArrayDepth.ts, 4, 47)) + +const result1: Value = flat(n); // Should infer T = Value, not T = Value[] +>result1 : Symbol(result1, Decl(inferenceUnionArrayDepth.ts, 9, 5)) +>Value : Symbol(Value, Decl(inferenceUnionArrayDepth.ts, 4, 47)) +>flat : Symbol(flat, Decl(inferenceUnionArrayDepth.ts, 0, 0)) +>n : Symbol(n, Decl(inferenceUnionArrayDepth.ts, 8, 13)) + +// Case 2: Object type (issue #3370) +type TG = { a: string }; +>TG : Symbol(TG, Decl(inferenceUnionArrayDepth.ts, 9, 31)) +>a : Symbol(a, Decl(inferenceUnionArrayDepth.ts, 12, 11)) + +function isNestedArray(arr: T[] | T[][]): arr is T[][] { +>isNestedArray : Symbol(isNestedArray, Decl(inferenceUnionArrayDepth.ts, 12, 24)) +>T : Symbol(T, Decl(inferenceUnionArrayDepth.ts, 14, 23)) +>arr : Symbol(arr, Decl(inferenceUnionArrayDepth.ts, 14, 26)) +>T : Symbol(T, Decl(inferenceUnionArrayDepth.ts, 14, 23)) +>T : Symbol(T, Decl(inferenceUnionArrayDepth.ts, 14, 23)) +>arr : Symbol(arr, Decl(inferenceUnionArrayDepth.ts, 14, 26)) +>T : Symbol(T, Decl(inferenceUnionArrayDepth.ts, 14, 23)) + + return Array.isArray(arr) && Array.isArray(arr[0]); +>Array.isArray : Symbol(ArrayConstructor.isArray, Decl(lib.es5.d.ts, --, --)) +>Array : Symbol(Array, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.core.d.ts, --, --), Decl(lib.es2015.iterable.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --) ... and 4 more) +>isArray : Symbol(ArrayConstructor.isArray, Decl(lib.es5.d.ts, --, --)) +>arr : Symbol(arr, Decl(inferenceUnionArrayDepth.ts, 14, 26)) +>Array.isArray : Symbol(ArrayConstructor.isArray, Decl(lib.es5.d.ts, --, --)) +>Array : Symbol(Array, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.core.d.ts, --, --), Decl(lib.es2015.iterable.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --) ... and 4 more) +>isArray : Symbol(ArrayConstructor.isArray, Decl(lib.es5.d.ts, --, --)) +>arr : Symbol(arr, Decl(inferenceUnionArrayDepth.ts, 14, 26)) +} + +function convert(controls: TG[] | TG[][]): TG[][] { +>convert : Symbol(convert, Decl(inferenceUnionArrayDepth.ts, 16, 1)) +>controls : Symbol(controls, Decl(inferenceUnionArrayDepth.ts, 18, 17)) +>TG : Symbol(TG, Decl(inferenceUnionArrayDepth.ts, 9, 31)) +>TG : Symbol(TG, Decl(inferenceUnionArrayDepth.ts, 9, 31)) +>TG : Symbol(TG, Decl(inferenceUnionArrayDepth.ts, 9, 31)) + + if (isNestedArray(controls)) { +>isNestedArray : Symbol(isNestedArray, Decl(inferenceUnionArrayDepth.ts, 12, 24)) +>controls : Symbol(controls, Decl(inferenceUnionArrayDepth.ts, 18, 17)) + + return controls; +>controls : Symbol(controls, Decl(inferenceUnionArrayDepth.ts, 18, 17)) + + } else { + return [controls]; +>controls : Symbol(controls, Decl(inferenceUnionArrayDepth.ts, 18, 17)) + } +} + +// Case 3: Primitive type (should already work) +declare const s: string[] | string[][]; +>s : Symbol(s, Decl(inferenceUnionArrayDepth.ts, 27, 13)) + +const result3: string = flat(s); +>result3 : Symbol(result3, Decl(inferenceUnionArrayDepth.ts, 28, 5)) +>flat : Symbol(flat, Decl(inferenceUnionArrayDepth.ts, 0, 0)) +>s : Symbol(s, Decl(inferenceUnionArrayDepth.ts, 27, 13)) + diff --git a/testdata/baselines/reference/compiler/inferenceUnionArrayDepth.types b/testdata/baselines/reference/compiler/inferenceUnionArrayDepth.types new file mode 100644 index 00000000000..9de13191f56 --- /dev/null +++ b/testdata/baselines/reference/compiler/inferenceUnionArrayDepth.types @@ -0,0 +1,78 @@ +//// [tests/cases/compiler/inferenceUnionArrayDepth.ts] //// + +=== inferenceUnionArrayDepth.ts === +// Regression test for https://github.com/microsoft/typescript-go/issues/1789 +// and https://github.com/microsoft/typescript-go/issues/3370 +// Inference should correctly infer T from T[] | T[][] union parameters. + +declare function flat(args: T[] | T[][]): T; +>flat : (args: T[] | T[][]) => T +>args : T[] | T[][] + +// Case 1: Union type (issue #1789) +type Value = 1 | 2; +>Value : Value + +declare const n: Value[] | Value[][]; +>n : Value[][] | Value[] + +const result1: Value = flat(n); // Should infer T = Value, not T = Value[] +>result1 : Value +>flat(n) : Value +>flat : (args: T[] | T[][]) => T +>n : Value[][] | Value[] + +// Case 2: Object type (issue #3370) +type TG = { a: string }; +>TG : TG +>a : string + +function isNestedArray(arr: T[] | T[][]): arr is T[][] { +>isNestedArray : (arr: T[] | T[][]) => arr is T[][] +>arr : T[] | T[][] + + return Array.isArray(arr) && Array.isArray(arr[0]); +>Array.isArray(arr) && Array.isArray(arr[0]) : boolean +>Array.isArray(arr) : boolean +>Array.isArray : (arg: any) => arg is any[] +>Array : ArrayConstructor +>isArray : (arg: any) => arg is any[] +>arr : T[] | T[][] +>Array.isArray(arr[0]) : boolean +>Array.isArray : (arg: any) => arg is any[] +>Array : ArrayConstructor +>isArray : (arg: any) => arg is any[] +>arr[0] : T | T[] +>arr : T[] | T[][] +>0 : 0 +} + +function convert(controls: TG[] | TG[][]): TG[][] { +>convert : (controls: TG[] | TG[][]) => TG[][] +>controls : TG[][] | TG[] + + if (isNestedArray(controls)) { +>isNestedArray(controls) : boolean +>isNestedArray : (arr: T[] | T[][]) => arr is T[][] +>controls : TG[][] | TG[] + + return controls; +>controls : TG[][] + + } else { + return [controls]; +>[controls] : TG[][] +>controls : TG[] + } +} + +// Case 3: Primitive type (should already work) +declare const s: string[] | string[][]; +>s : string[] | string[][] + +const result3: string = flat(s); +>result3 : string +>flat(s) : string +>flat : (args: T[] | T[][]) => T +>s : string[] | string[][] + diff --git a/testdata/tests/cases/compiler/inferenceUnionArrayDepth.ts b/testdata/tests/cases/compiler/inferenceUnionArrayDepth.ts new file mode 100644 index 00000000000..f7cc31b43c9 --- /dev/null +++ b/testdata/tests/cases/compiler/inferenceUnionArrayDepth.ts @@ -0,0 +1,31 @@ +// @strict: true + +// Regression test for https://github.com/microsoft/typescript-go/issues/1789 +// and https://github.com/microsoft/typescript-go/issues/3370 +// Inference should correctly infer T from T[] | T[][] union parameters. + +declare function flat(args: T[] | T[][]): T; + +// Case 1: Union type (issue #1789) +type Value = 1 | 2; +declare const n: Value[] | Value[][]; +const result1: Value = flat(n); // Should infer T = Value, not T = Value[] + +// Case 2: Object type (issue #3370) +type TG = { a: string }; + +function isNestedArray(arr: T[] | T[][]): arr is T[][] { + return Array.isArray(arr) && Array.isArray(arr[0]); +} + +function convert(controls: TG[] | TG[][]): TG[][] { + if (isNestedArray(controls)) { + return controls; + } else { + return [controls]; + } +} + +// Case 3: Primitive type (should already work) +declare const s: string[] | string[][]; +const result3: string = flat(s); From 70f6cb2638e8febf90582c577608ad53d1e91bab Mon Sep 17 00:00:00 2001 From: Yiin Date: Sun, 12 Apr 2026 18:31:27 +0300 Subject: [PATCH 2/3] Remove trailing newline to pass gofmt check --- internal/checker/inference.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/checker/inference.go b/internal/checker/inference.go index 8729d7ddcc5..4b714434f83 100644 --- a/internal/checker/inference.go +++ b/internal/checker/inference.go @@ -1642,4 +1642,3 @@ func (c *Checker) mergeInferences(target []*InferenceInfo, source []*InferenceIn } } } - From 31264a4cf3fb9a5f8536dedeaa199afe4e984439 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Sat, 13 Jun 2026 18:12:34 -0700 Subject: [PATCH 3/3] Address PR feedback on deep union matching --- internal/checker/inference.go | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/internal/checker/inference.go b/internal/checker/inference.go index 30bbcd1bccb..b97d0e9e09e 100644 --- a/internal/checker/inference.go +++ b/internal/checker/inference.go @@ -110,7 +110,7 @@ func (c *Checker) inferFromTypes(n *InferenceState, source *Type, target *Type) tempSources, tempTargets := c.inferFromMatchingTypes(n, sourceTypes, target.Distributed(), (*Checker).isTypeOrBaseIdenticalTo) // Next, infer between deeply matching source and target constituents and remove the // matching types. Types deeply match when they are instantiations of the same object - // type AND their type arguments are themselves closely matched. This prevents incorrect + // type and their type arguments have matching nesting structure. This prevents incorrect // cross-matching of types like Array with Array in unions like T[] | T[][]. tempSources2, tempTargets2 := c.inferFromMatchingTypes(n, tempSources, tempTargets, (*Checker).isTypeDeeplyMatchedBy) // Next, infer between closely matching source and target constituents and remove @@ -1176,7 +1176,7 @@ func (c *Checker) isTypeCloselyMatchedBy(s *Type, t *Type) bool { // isTypeDeeplyMatchedBy checks that two types are closely matched AND that their type arguments // have compatible nesting depth. This provides a more precise matching than isTypeCloselyMatchedBy // for cases like T[] | T[][] where all constituents share the same symbol (Array) but differ in -// nesting depth — preventing cross-matching of Array with Array>. +// nesting depth, preventing cross-matching of Array with Array>. func (c *Checker) isTypeDeeplyMatchedBy(s *Type, t *Type) bool { if !c.isTypeCloselyMatchedBy(s, t) { return false @@ -1184,18 +1184,19 @@ func (c *Checker) isTypeDeeplyMatchedBy(s *Type, t *Type) bool { if s.objectFlags&ObjectFlagsReference != 0 && t.objectFlags&ObjectFlagsReference != 0 { sr := s.AsTypeReference() tr := t.AsTypeReference() - sArgs := sr.resolvedTypeArguments - tArgs := tr.resolvedTypeArguments - if len(sArgs) == len(tArgs) { - for i := range sArgs { - // Check that type arguments have matching nesting structure: if the source arg - // is a nested reference to the same outer type (e.g., Array>), the - // target arg must also be nested, and vice versa. - saIsNestedRef := sArgs[i].objectFlags&ObjectFlagsReference != 0 && sArgs[i].AsTypeReference().target == sr.target - taIsNestedRef := tArgs[i].objectFlags&ObjectFlagsReference != 0 && tArgs[i].AsTypeReference().target == tr.target - if saIsNestedRef != taIsNestedRef { - return false - } + sArgs := c.getTypeArguments(s) + tArgs := c.getTypeArguments(t) + if len(sArgs) != len(tArgs) { + return false + } + for i := range sArgs { + // Check that type arguments have matching nesting structure: if the source arg + // is a nested reference to the same outer type (e.g., Array>), the + // target arg must also be nested, and vice versa. + saIsNestedRef := sArgs[i].objectFlags&ObjectFlagsReference != 0 && sArgs[i].AsTypeReference().target == sr.target + taIsNestedRef := tArgs[i].objectFlags&ObjectFlagsReference != 0 && tArgs[i].AsTypeReference().target == tr.target + if saIsNestedRef != taIsNestedRef { + return false } } }