Skip to content

Fix inference from unions of arrays with different nesting depths#3391

Open
Yiin wants to merge 2 commits intomicrosoft:mainfrom
Yiin:fix/inference-union-array-depth
Open

Fix inference from unions of arrays with different nesting depths#3391
Yiin wants to merge 2 commits intomicrosoft:mainfrom
Yiin:fix/inference-union-array-depth

Conversation

@Yiin
Copy link
Copy Markdown

@Yiin Yiin commented Apr 12, 2026

Fixes #1789, fixes #3370.

When inferring T from a parameter typed T[] | T[][], the closely-matched inference pass cross-matches all Array constituents regardless of nesting depth. CompareTypes sorts union constituents by type flags (Object < Union), so 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.

This PR adds an intermediate "deeply matched" pass between the existing isTypeOrBaseIdenticalTo and isTypeCloselyMatchedBy passes. The new 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<Array<...>> inside Array), the target arg must also be nested, and vice versa. Same-depth pairs are inferred first, preventing the incorrect cross-match.

declare function flat<T>(args: T[] | T[][]): T;
type Value = 1 | 2;
declare const n: Value[] | Value[][];

// Before: T inferred as Value[] (error)
// After:  T inferred as Value   (correct)
flat(n);

Notes

  • The ordering bug also exists in TypeScript when stableTypeOrdering is enabled (the default since it was added in Feb 2026), but no upstream test covers the T[] | T[][] pattern with non-primitive type arguments.
  • The extra matching pass is O(sources × targets) with cheap flag/pointer checks. Union types have few constituents in practice, and pairs consumed by the deep pass are removed before the close pass, so total inferFromTypes calls don't increase.
  • All local, submodule, and checker tests pass with no regressions.

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<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 microsoft#1789, fixes microsoft#3370.
@Yiin
Copy link
Copy Markdown
Author

Yiin commented Apr 12, 2026

@microsoft-github-policy-service agree

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bad inference for param of type T[] | T[][] Inference fails form unions of arrays of different depths (T[] | T[][])

1 participant