Skip to content
Open
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
37 changes: 36 additions & 1 deletion internal/checker/inference.go
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> with Array<T[]> 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
}
Expand Down Expand Up @@ -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<Value> with Array<Array<T>>.
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<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
}
Comment on lines +1184 to +1204

// 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)
Expand Down
52 changes: 52 additions & 0 deletions testdata/baselines/reference/compiler/inferenceUnionArrayDepth.js
Original file line number Diff line number Diff line change
@@ -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<T>(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<T>(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);
Original file line number Diff line number Diff line change
@@ -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<T>(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<T>(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))

Original file line number Diff line number Diff line change
@@ -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<T>(args: T[] | T[][]): T;
>flat : <T>(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 : <T>(args: T[] | T[][]) => T
>n : Value[][] | Value[]

// Case 2: Object type (issue #3370)
type TG = { a: string };
>TG : TG
>a : string

function isNestedArray<T>(arr: T[] | T[][]): arr is T[][] {
>isNestedArray : <T>(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 : <T>(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 : <T>(args: T[] | T[][]) => T
>s : string[] | string[][]

31 changes: 31 additions & 0 deletions testdata/tests/cases/compiler/inferenceUnionArrayDepth.ts
Original file line number Diff line number Diff line change
@@ -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<T>(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<T>(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);