diff --git a/internal/checker/inference.go b/internal/checker/inference.go index fb5da8b8226..b97d0e9e09e 100644 --- a/internal/checker/inference.go +++ b/internal/checker/inference.go @@ -108,10 +108,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, 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 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 // 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 } @@ -1168,6 +1173,36 @@ 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 := 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 + } + } + } + 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) 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);