Skip to content

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

Open
Yiin wants to merge 6 commits into
microsoft:mainfrom
Yiin:fix/inference-union-array-depth
Open

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

Conversation

@Yiin

@Yiin Yiin commented Apr 12, 2026

Copy link
Copy Markdown

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

Yiin commented Apr 12, 2026

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

# Conflicts:
#	internal/checker/inference.go
Copilot AI review requested due to automatic review settings June 12, 2026 22:56
@jakebailey

Copy link
Copy Markdown
Member

@typescript-bot test it

@typescript-automation

typescript-automation Bot commented Jun 12, 2026

Copy link
Copy Markdown

Starting jobs; this comment will be updated as builds start and complete.

Command Status Results
test top400 ✅ Started ✅ Results
perf test this faster ✅ Started 👀 Results

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a TypeScript-go type inference bug when inferring T from union parameters like T[] | T[][], where union constituent ordering could cause a mismatched array-depth cross-match and infer T one level off.

Changes:

  • Adds a new “deep match” inference pass between identical-match and close-match passes to prefer same-nesting matches.
  • Introduces a regression compiler test covering union, object, and primitive cases for T[] | T[][] inference.
  • Adds new reference baselines for the regression test (.types, .symbols, .js).

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
internal/checker/inference.go Adds a new deep-matching pass and a helper predicate to avoid cross-matching arrays with different nesting depths.
testdata/tests/cases/compiler/inferenceUnionArrayDepth.ts New regression test exercising the problematic `T[]
testdata/baselines/reference/compiler/inferenceUnionArrayDepth.types Baseline capturing expected inferred types for the new regression test.
testdata/baselines/reference/compiler/inferenceUnionArrayDepth.symbols Baseline capturing expected symbol info for the new regression test.
testdata/baselines/reference/compiler/inferenceUnionArrayDepth.js Baseline capturing expected emit output for the new regression test.

Comment on lines +1184 to +1201
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<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
}
}
}
}
Comment on lines +111 to +114
// 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<T> with Array<T[]> in unions like T[] | T[][].
@typescript-automation

Copy link
Copy Markdown

@jakebailey
The results of the perf run you requested are in!

Here they are:

tsc

Comparison Report - baseline..pr
Metric baseline pr Delta Best Worst p-value
Compiler-Unions - native
Errors 4 4 ~ ~ ~ p=1.000 n=6
Symbols 81,800 (± 0.03%) 81,787 (± 0.05%) ~ 81,711 81,813 p=0.630 n=6
Types 98,821 98,821 ~ ~ ~ p=1.000 n=6
Memory Used 179,905k (± 0.27%) 180,166k (± 0.23%) ~ 179,716k 180,694k p=0.575 n=6
Memory Allocs 1,564,779 (± 0.01%) 1,564,823 (± 0.01%) ~ 1,564,602 1,565,155 p=1.000 n=6
Config Time 0.000s 0.000s (±154.76%) ~ 0.000s 0.001s p=0.174 n=6
Parse Time 0.061s (± 6.57%) 0.060s (± 9.64%) ~ 0.049s 0.066s p=0.936 n=6
Bind Time 0.017s (±14.78%) 0.016s 🟩-0.001s (- 8.57%) ~ ~ p=0.048 n=6
Check Time 0s 0s ~ ~ ~ p=1.000 n=6
Emit Time 0.847s (± 2.89%) 0.842s (± 0.70%) ~ 0.837s 0.852s p=1.000 n=6
Total Time 0.926s (± 2.48%) 0.919s (± 0.50%) ~ 0.915s 0.928s p=1.000 n=6
angular-1 - native
Errors 3 3 ~ ~ ~ p=1.000 n=6
Symbols 885,204 (± 0.07%) 884,461 (± 0.09%) ~ 883,426 885,732 p=0.128 n=6
Types 263,768 (± 0.00%) 263,768 (± 0.00%) ~ 263,766 263,769 p=0.720 n=6
Memory Used 830,880k (± 0.04%) 830,361k (± 0.03%) -520k (- 0.06%) 829,833k 830,627k p=0.020 n=6
Memory Allocs 6,807,258 (± 0.27%) 6,806,401 (± 0.20%) ~ 6,794,209 6,822,962 p=0.936 n=6
Config Time 0.017s 0.017s (± 2.98%) ~ 0.017s 0.018s p=0.174 n=6
Parse Time 0.254s (± 4.49%) 0.249s (± 2.75%) ~ 0.240s 0.256s p=0.378 n=6
Bind Time 0.055s (±39.53%) 0.046s (±39.59%) ~ 0.034s 0.080s p=0.520 n=6
Check Time 0s 0s ~ ~ ~ p=1.000 n=6
Emit Time 1.889s (± 1.76%) 1.873s (± 1.27%) ~ 1.844s 1.917s p=0.471 n=6
Total Time 2.227s (± 0.58%) 2.198s (± 1.49%) ~ 2.158s 2.237s p=0.199 n=6
mui-docs - native
Errors 11,244 (± 0.03%) 11,244 (± 0.03%) ~ 11,239 11,246 p=1.000 n=6
Symbols 4,212,918 4,212,918 ~ ~ ~ p=1.000 n=6
Types 1,532,172 1,532,172 ~ ~ ~ p=1.000 n=6
Memory Used 4,963,045k (± 0.06%) 4,962,724k (± 0.08%) ~ 4,957,494k 4,966,464k p=0.936 n=6
Memory Allocs 92,571,396 (±19.06%) 92,344,197 (±33.17%) ~ 61,878,566 130,622,439 p=1.000 n=6
Config Time 0.016s (± 2.52%) 0.016s (± 3.16%) ~ 0.016s 0.017s p=0.595 n=6
Parse Time 1.191s (±26.24%) 1.229s (±42.21%) ~ 0.718s 1.869s p=1.000 n=6
Bind Time 0.002s (±18.82%) 0.002s ~ ~ ~ p=0.405 n=6
Check Time 17.062s (± 0.33%) 17.030s (± 0.43%) ~ 16.962s 17.132s p=0.378 n=6
Emit Time 0.442s (± 2.97%) 0.454s (± 4.42%) ~ 0.436s 0.481s p=0.374 n=6
Total Time 19.365s (± 1.75%) 19.386s (± 3.05%) ~ 18.617s 20.161s p=1.000 n=6
self-build-src - native
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 1,396,711 1,396,711 ~ ~ ~ p=1.000 n=6
Types 442,161 442,161 ~ ~ ~ p=1.000 n=6
Memory Used 1,646,838k (± 0.28%) 1,647,053k (± 0.20%) ~ 1,641,866k 1,650,658k p=0.936 n=6
Memory Allocs 54,860,809 (± 0.05%) 54,866,157 (± 0.05%) ~ 54,824,819 54,895,211 p=0.936 n=6
Config Time 0.013s (±32.34%) 0.015s (±31.71%) ~ 0.006s 0.019s p=0.470 n=6
Parse Time 0.254s (± 3.96%) 0.258s (± 3.16%) ~ 0.246s 0.271s p=0.810 n=6
Bind Time 0.000s 0.000s (±244.70%) ~ 0.000s 0.002s p=0.405 n=6
Check Time 2.310s (± 0.47%) 2.321s (± 0.97%) ~ 2.294s 2.350s p=0.471 n=6
Emit Time 0.224s (± 4.31%) 0.226s (± 3.28%) ~ 0.219s 0.238s p=0.630 n=6
Total Time 29.280s (± 0.79%) 29.508s (± 0.75%) ~ 29.182s 29.775s p=0.128 n=6
self-compiler - native
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 338,176 338,176 ~ ~ ~ p=1.000 n=6
Types 199,522 199,522 ~ ~ ~ p=1.000 n=6
Memory Used 332,134k (± 0.03%) 332,161k (± 0.05%) ~ 331,917k 332,376k p=0.873 n=6
Memory Allocs 2,369,724 (± 0.02%) 2,369,730 (± 0.04%) ~ 2,368,420 2,370,740 p=1.000 n=6
Config Time 0.001s 0.001s ~ ~ ~ p=1.000 n=6
Parse Time 0.128s (± 2.56%) 0.132s (± 4.53%) ~ 0.123s 0.138s p=0.199 n=6
Bind Time 0.000s 0.000s ~ ~ ~ p=1.000 n=6
Check Time 1.389s (± 1.59%) 1.374s (± 0.79%) ~ 1.361s 1.386s p=0.335 n=6
Emit Time 0.090s (±13.41%) 0.089s (± 9.92%) ~ 0.080s 0.105s p=1.000 n=6
Total Time 1.659s (± 0.85%) 1.647s (± 0.95%) ~ 1.625s 1.668s p=0.172 n=6
ts-pre-modules - native
Errors 3 3 ~ ~ ~ p=1.000 n=6
Symbols 97,488 97,488 ~ ~ ~ p=1.000 n=6
Types 356 356 ~ ~ ~ p=1.000 n=6
Memory Used 133,435k (± 0.03%) 133,445k (± 0.04%) ~ 133,385k 133,510k p=0.630 n=6
Memory Allocs 183,063 (± 0.20%) 183,061 (± 0.14%) ~ 182,787 183,416 p=0.689 n=6
Config Time 0.001s 0.001s ~ ~ ~ p=1.000 n=6
Parse Time 0.113s (± 2.98%) 0.114s (± 6.82%) ~ 0.109s 0.128s p=0.871 n=6
Bind Time 0.036s (±12.09%) 0.039s (±10.99%) ~ 0.034s 0.046s p=0.467 n=6
Check Time 0s 0s ~ ~ ~ p=1.000 n=6
Emit Time 0.000s 0.000s ~ ~ ~ p=1.000 n=6
Total Time 0.153s (± 1.41%) 0.158s (± 5.43%) ~ 0.147s 0.168s p=0.629 n=6
vscode - native
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 6,648,586 6,648,586 ~ ~ ~ p=1.000 n=6
Types 2,339,491 2,339,491 ~ ~ ~ p=1.000 n=6
Memory Used 4,496,744k (± 0.03%) 4,496,881k (± 0.03%) ~ 4,495,212k 4,498,768k p=0.810 n=6
Memory Allocs 31,653,656 (± 0.10%) 31,668,497 (± 0.12%) ~ 31,635,721 31,720,857 p=0.298 n=6
Config Time 0.074s (±13.85%) 0.075s (± 6.63%) ~ 0.070s 0.083s p=0.688 n=6
Parse Time 0.879s (± 2.81%) 0.886s (± 4.61%) ~ 0.815s 0.929s p=0.575 n=6
Bind Time 0.134s (±10.04%) 0.130s (± 1.98%) ~ 0.126s 0.133s p=1.000 n=6
Check Time 8.499s (± 0.56%) 8.482s (± 0.51%) ~ 8.432s 8.547s p=0.575 n=6
Emit Time 2.278s (± 3.55%) 2.236s (± 3.10%) ~ 2.173s 2.365s p=0.378 n=6
Total Time 11.885s (± 1.12%) 11.826s (± 0.75%) ~ 11.718s 11.948s p=0.336 n=6
webpack - native
Errors 2 2 ~ ~ ~ p=1.000 n=6
Symbols 181,636 181,636 ~ ~ ~ p=1.000 n=6
Types 340 340 ~ ~ ~ p=1.000 n=6
Memory Used 219,068k (± 0.11%) 219,256k (± 0.13%) ~ 218,935k 219,684k p=0.298 n=6
Memory Allocs 1,057,897 (± 0.32%) 1,060,799 (± 0.36%) ~ 1,054,313 1,064,958 p=0.230 n=6
Config Time 0.012s (± 8.84%) 0.010s (±25.03%) ~ 0.007s 0.013s p=0.190 n=6
Parse Time 0.150s (± 2.73%) 0.151s (± 1.61%) ~ 0.146s 0.153s p=0.806 n=6
Bind Time 0s 0s ~ ~ ~ p=1.000 n=6
Check Time 0s 0s ~ ~ ~ p=1.000 n=6
Emit Time 0.041s (±15.04%) 0.043s (±15.07%) ~ 0.036s 0.050s p=1.000 n=6
Total Time 0.203s (± 3.13%) 0.204s (± 3.25%) ~ 0.197s 0.215s p=0.936 n=6
xstate-main - native
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 1,063,468 1,063,468 ~ ~ ~ p=1.000 n=6
Types 387,762 387,762 ~ ~ ~ p=1.000 n=6
Memory Used 641,514k (± 0.02%) 641,570k (± 0.02%) ~ 641,424k 641,747k p=0.689 n=6
Memory Allocs 5,044,006 (± 0.06%) 5,043,581 (± 0.08%) ~ 5,037,391 5,048,312 p=1.000 n=6
Config Time 0.005s 0.004s (±28.03%) ~ 0.002s 0.005s p=0.073 n=6
Parse Time 0.133s (± 4.04%) 0.135s (± 3.95%) ~ 0.129s 0.144s p=0.809 n=6
Bind Time 0.034s (±29.47%) 0.038s (±22.49%) ~ 0.028s 0.046s p=0.520 n=6
Check Time 1.294s (± 1.22%) 1.293s (± 0.90%) ~ 1.282s 1.312s p=1.000 n=6
Emit Time 0.001s 0.001s ~ ~ ~ p=1.000 n=6
Total Time 1.471s (± 0.72%) 1.476s (± 1.35%) ~ 1.459s 1.506s p=0.936 n=6
System info unknown
Hosts
  • native
Scenarios
  • Compiler-Unions - native
  • angular-1 - native
  • mui-docs - native
  • self-build-src - native
  • self-compiler - native
  • ts-pre-modules - native
  • vscode - native
  • webpack - native
  • xstate-main - native
Benchmark Name Iterations
Current pr 6
Baseline baseline 6

Developer Information:

Download Benchmarks

@typescript-automation

Copy link
Copy Markdown

@jakebailey Here are the results of running the top 400 repos with tsc comparing main and refs/pull/3391/merge:

Everything looks good!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Comment on lines +1184 to +1204
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
}
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[][])

5 participants