Skip to content

Properly account for intersections in getKeyPropertyName#63117

Open
Andarist wants to merge 1 commit intomicrosoft:mainfrom
Andarist:fix/union-optimization-for-intersections
Open

Properly account for intersections in getKeyPropertyName#63117
Andarist wants to merge 1 commit intomicrosoft:mainfrom
Andarist:fix/union-optimization-for-intersections

Conversation

@Andarist
Copy link
Contributor

@Andarist Andarist commented Feb 8, 2026

In the code below, getMatchingUnionConstituentForType (and thus typeRelatedToSomeType) can't benefit from the key property optimization. So in certain situations (like this one) the source has to be related to each constituent of the target union (until it finds a match) - but it should be able to find the matching constituent using a fast path.

This issue affects test1. I've included test2 for comparison purposes - the optimization kicks in there.

TS playground:

type Identity<T> = { [K in keyof T]: T[K] } & unknown;

type A = { kind: "a" } & { a: string };
type B = { kind: "b" } & { b: string };
type C = { kind: "c" } & { c: string };
type D = { kind: "d" } & { d: string };
type E = { kind: "e" } & { e: string };
type F = { kind: "f" } & { f: string };
type G = { kind: "g" } & { g: string };
type H = { kind: "h" } & { h: string };
type I = { kind: "i" } & { i: string };
type J = { kind: "j" } & { j: string };
type K = { kind: "k" } & { k: string };
type L = { kind: "l" } & { l: string };
type M = { kind: "m" } & { m: string };
type N = { kind: "n" } & { n: string };
type O = { kind: "o" } & { o: string };
type P = { kind: "p" } & { p: string };
type Q = { kind: "q" } & { q: string };
type R = { kind: "r" } & { r: string };
type S = { kind: "s" } & { s: string };
type T = { kind: "t" } & { t: string };
type U = { kind: "u" } & { u: string };
type V = { kind: "v" } & { v: string };
type W = { kind: "w" } & { w: string };
type X = { kind: "x" } & { x: string };
type Y = { kind: "y" } & { y: string };
type Z = { kind: "z" } & { z: string };

type MyUnion = A | B | C | D | E | F | G | H | I | J | K | L | M | N | O | P | Q | R | S | T | U | V | W | X | Y | Z;

type SomeType = { kind: "z"; z: string; other: number };

function test1(u1: MyUnion, obj: SomeType) {
  u1 = obj;
}

// using Identity here just to "flatten" the contained intersections
function test2(u1: Identity<MyUnion>, obj: SomeType) {
  u1 = obj;
}

@github-project-automation github-project-automation bot moved this to Not started in PR Backlog Feb 8, 2026
@typescript-bot typescript-bot added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Feb 8, 2026
@typescript-bot
Copy link
Collaborator

This PR doesn't have any linked issues. Please open an issue that references this PR. From there we can discuss and prioritise.

if (
types.length < 10 || getObjectFlags(unionType) & ObjectFlags.PrimitiveUnion ||
countWhere(types, t => !!(t.flags & (TypeFlags.Object | TypeFlags.InstantiableNonPrimitive))) < 10
countWhere(types, t => !!(t.flags & (TypeFlags.Object | TypeFlags.Intersection | TypeFlags.InstantiableNonPrimitive))) < 10
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This condition matches now the existing flags check in mapTypesByKeyProperty (that function is already called only from within the containing function here - getKeyPropertyName)

@jakebailey
Copy link
Member

@typescript-bot test it

@typescript-bot
Copy link
Collaborator

typescript-bot commented Feb 8, 2026

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

Command Status Results
test top400 ✅ Started ✅ Results
user test this ✅ Started ✅ Results
run dt ✅ Started ✅ Results
perf test this faster ✅ Started 👀 Results

@typescript-bot
Copy link
Collaborator

Hey @jakebailey, the results of running the DT tests are ready.

Everything looks the same!

You can check the log here.

@typescript-bot
Copy link
Collaborator

@jakebailey Here are the results of running the user tests with tsc comparing main and refs/pull/63117/merge:

There were infrastructure failures potentially unrelated to your change:

  • 1 instance of "Git clone failed"

Otherwise...

Everything looks good!

@typescript-bot
Copy link
Collaborator

@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 - node (v18.15.0, x64)
Errors 3 3 ~ ~ ~ p=1.000 n=6
Symbols 82,521 82,521 ~ ~ ~ p=1.000 n=6
Types 125,280 125,280 ~ ~ ~ p=1.000 n=6
Memory used 276,131k (± 0.68%) 274,975k (± 0.50%) ~ 274,399k 277,803k p=0.575 n=6
Parse Time 1.30s (± 0.40%) 1.31s (± 0.31%) ~ 1.30s 1.31s p=0.112 n=6
Bind Time 0.75s (± 0.68%) 0.75s (± 0.72%) ~ 0.75s 0.76s p=0.640 n=6
Check Time 14.16s (± 0.60%) 14.14s (± 0.06%) ~ 14.13s 14.15s p=0.373 n=6
Emit Time 2.66s (± 0.84%) 2.66s (± 0.58%) ~ 2.65s 2.69s p=0.604 n=6
Total Time 18.88s (± 0.46%) 18.86s (± 0.09%) ~ 18.84s 18.89s p=0.743 n=6
angular-1 - node (v18.15.0, x64)
Errors 3 3 ~ ~ ~ p=1.000 n=6
Symbols 957,952 957,952 ~ ~ ~ p=1.000 n=6
Types 416,021 416,019 -2 (- 0.00%) ~ ~ p=0.001 n=6
Memory used 1,259,098k (± 0.00%) 1,259,061k (± 0.01%) ~ 1,258,963k 1,259,164k p=0.575 n=6
Parse Time 6.52s (± 0.64%) 6.52s (± 0.66%) ~ 6.47s 6.57s p=0.936 n=6
Bind Time 1.98s (± 0.38%) 1.98s (± 0.41%) ~ 1.97s 1.99s p=0.729 n=6
Check Time 32.41s (± 0.30%) 32.38s (± 0.47%) ~ 32.21s 32.63s p=0.689 n=6
Emit Time 14.99s (± 0.65%) 14.96s (± 0.53%) ~ 14.85s 15.07s p=0.688 n=6
Total Time 55.89s (± 0.32%) 55.84s (± 0.42%) ~ 55.59s 56.26s p=0.630 n=6
mui-docs - node (v18.15.0, x64)
Errors 11,439 11,439 ~ ~ ~ p=1.000 n=6
Symbols 2,702,777 2,710,413 +7,636 (+ 0.28%) ~ ~ p=0.001 n=6
Types 930,725 947,613 +16,888 (+ 1.81%) ~ ~ p=0.001 n=6
Memory used 3,043,677k (± 0.00%) 3,057,722k (± 0.00%) +14,045k (+ 0.46%) 3,057,577k 3,057,871k p=0.005 n=6
Parse Time 10.38s (± 0.45%) 10.31s (± 0.33%) -0.06s (- 0.61%) 10.27s 10.37s p=0.030 n=6
Bind Time 2.78s (± 0.53%) 2.78s (± 0.48%) ~ 2.76s 2.80s p=1.000 n=6
Check Time 114.25s (± 1.38%) 115.26s (± 1.29%) ~ 114.06s 117.31s p=0.378 n=6
Emit Time 0.41s (± 2.59%) 0.40s (± 1.39%) ~ 0.39s 0.40s p=0.090 n=6
Total Time 127.81s (± 1.25%) 128.75s (± 1.15%) ~ 127.54s 130.79s p=0.378 n=6
self-build-src - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 1,253,407 1,253,407 ~ ~ ~ p=1.000 n=6
Types 260,208 260,207 -1 (- 0.00%) ~ ~ p=0.001 n=6
Memory used 2,396,836k (± 0.04%) 2,457,458k (± 6.06%) ~ 2,396,224k 2,761,611k p=0.575 n=6
Parse Time 5.14s (± 0.70%) 5.18s (± 0.77%) ~ 5.13s 5.23s p=0.199 n=6
Bind Time 1.84s (± 1.52%) 1.84s (± 1.53%) ~ 1.81s 1.87s p=0.677 n=6
Check Time 35.72s (± 0.45%) 35.60s (± 0.85%) ~ 34.99s 35.76s p=0.226 n=6
Emit Time 2.99s (± 1.80%) 3.07s (± 2.16%) +0.08s (+ 2.67%) 3.00s 3.19s p=0.037 n=6
Total Time 45.73s (± 0.37%) 45.71s (± 0.67%) ~ 45.10s 45.97s p=0.810 n=6
self-build-src-public-api - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 1,253,407 1,253,407 ~ ~ ~ p=1.000 n=6
Types 260,208 260,207 -1 (- 0.00%) ~ ~ p=0.001 n=6
Memory used 2,523,836k (± 5.88%) 2,584,227k (±11.49%) ~ 2,462,601k 3,191,074k p=0.230 n=6
Parse Time 5.34s (± 0.42%) 5.37s (± 1.42%) ~ 5.28s 5.47s p=0.630 n=6
Bind Time 1.88s (± 0.62%) 1.88s (± 0.92%) ~ 1.86s 1.90s p=0.935 n=6
Check Time 35.62s (± 0.58%) 35.77s (± 0.47%) ~ 35.48s 35.99s p=0.149 n=6
Emit Time 3.07s (± 1.57%) 3.06s (± 1.44%) ~ 3.01s 3.13s p=0.469 n=6
Total Time 45.93s (± 0.54%) 46.09s (± 0.42%) ~ 45.81s 46.38s p=0.230 n=6
self-compiler - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 265,173 265,173 ~ ~ ~ p=1.000 n=6
Types 104,282 104,282 ~ ~ ~ p=1.000 n=6
Memory used 444,503k (± 0.02%) 444,541k (± 0.01%) ~ 444,482k 444,574k p=0.471 n=6
Parse Time 4.35s (± 0.67%) 4.37s (± 0.72%) ~ 4.31s 4.40s p=0.421 n=6
Bind Time 1.70s (± 0.87%) 1.69s (± 1.02%) ~ 1.66s 1.71s p=0.742 n=6
Check Time 23.94s (± 0.19%) 23.86s (± 0.46%) ~ 23.74s 24.02s p=0.128 n=6
Emit Time 1.93s (± 0.76%) 1.93s (± 1.02%) ~ 1.91s 1.96s p=1.000 n=6
Total Time 31.92s (± 0.16%) 31.85s (± 0.32%) ~ 31.73s 31.99s p=0.261 n=6
ts-pre-modules - node (v18.15.0, x64)
Errors 271 271 ~ ~ ~ p=1.000 n=6
Symbols 225,885 225,885 ~ ~ ~ p=1.000 n=6
Types 93,564 93,564 ~ ~ ~ p=1.000 n=6
Memory used 371,621k (± 0.03%) 371,664k (± 0.02%) ~ 371,579k 371,787k p=0.128 n=6
Parse Time 2.84s (± 1.20%) 2.84s (± 1.11%) ~ 2.80s 2.88s p=0.936 n=6
Bind Time 1.62s (± 0.94%) 1.64s (± 0.64%) +0.02s (+ 1.24%) 1.62s 1.65s p=0.042 n=6
Check Time 17.45s (± 0.29%) 17.43s (± 0.28%) ~ 17.36s 17.50s p=0.520 n=6
Emit Time 0.00s 0.00s ~ ~ ~ p=1.000 n=6
Total Time 21.91s (± 0.19%) 21.90s (± 0.36%) ~ 21.81s 22.03s p=0.688 n=6
vscode - node (v18.15.0, x64)
Errors 40 40 ~ ~ ~ p=1.000 n=6
Symbols 4,235,336 4,235,310 -26 (- 0.00%) ~ ~ p=0.001 n=6
Types 1,345,132 1,345,130 -2 (- 0.00%) ~ ~ p=0.001 n=6
Memory used 4,027,988k (± 0.00%) 4,028,001k (± 0.00%) ~ 4,027,825k 4,028,117k p=0.689 n=6
Parse Time 16.30s (± 0.63%) 16.28s (± 0.26%) ~ 16.23s 16.34s p=0.574 n=6
Bind Time 5.55s (± 2.25%) 5.53s (± 1.79%) ~ 5.47s 5.73s p=0.293 n=6
Check Time 127.18s (± 7.30%) 126.35s (± 5.70%) ~ 121.60s 140.42s p=0.689 n=6
Emit Time 51.69s (±14.03%) 52.56s (±15.87%) ~ 45.54s 65.49s p=0.810 n=6
Total Time 200.72s (± 7.98%) 200.72s (± 7.46%) ~ 190.10s 227.76s p=0.689 n=6
webpack - node (v18.15.0, x64)
Errors 41 41 ~ ~ ~ p=1.000 n=6
Symbols 403,253 403,369 +116 (+ 0.03%) ~ ~ p=0.001 n=6
Types 178,748 178,764 +16 (+ 0.01%) ~ ~ p=0.001 n=6
Memory used 561,100k (± 0.02%) 561,255k (± 0.05%) ~ 561,039k 561,822k p=0.298 n=6
Parse Time 4.77s (± 0.68%) 4.75s (± 0.38%) ~ 4.72s 4.77s p=0.124 n=6
Bind Time 2.03s (± 1.22%) 2.02s (± 0.85%) ~ 2.00s 2.04s p=0.459 n=6
Check Time 24.51s (± 0.30%) 24.44s (± 0.75%) ~ 24.10s 24.65s p=0.630 n=6
Emit Time 0.01s (±109.43%) 0.01s (±48.94%) ~ 0.00s 0.01s p=0.282 n=6
Total Time 31.32s (± 0.17%) 31.22s (± 0.57%) ~ 30.88s 31.41s p=0.170 n=6
xstate-main - node (v18.15.0, x64)
Errors 30 30 ~ ~ ~ p=1.000 n=6
Symbols 753,485 753,622 +137 (+ 0.02%) ~ ~ p=0.001 n=6
Types 215,172 215,193 +21 (+ 0.01%) ~ ~ p=0.001 n=6
Memory used 678,560k (± 0.01%) 678,634k (± 0.01%) +74k (+ 0.01%) 678,555k 678,679k p=0.045 n=6
Parse Time 4.91s (± 0.66%) 4.91s (± 0.60%) ~ 4.86s 4.94s p=1.000 n=6
Bind Time 1.58s (± 0.98%) 1.56s (± 0.75%) ~ 1.55s 1.58s p=0.061 n=6
Check Time 22.21s (± 0.44%) 22.35s (± 0.58%) ~ 22.23s 22.58s p=0.054 n=6
Emit Time 0.00s 0.00s ~ ~ ~ p=1.000 n=6
Total Time 28.70s (± 0.33%) 28.81s (± 0.39%) ~ 28.71s 29.02s p=0.077 n=6
System info unknown
Hosts
  • node (v18.15.0, x64)
Scenarios
  • Compiler-Unions - node (v18.15.0, x64)
  • angular-1 - node (v18.15.0, x64)
  • mui-docs - node (v18.15.0, x64)
  • self-build-src - node (v18.15.0, x64)
  • self-build-src-public-api - node (v18.15.0, x64)
  • self-compiler - node (v18.15.0, x64)
  • ts-pre-modules - node (v18.15.0, x64)
  • vscode - node (v18.15.0, x64)
  • webpack - node (v18.15.0, x64)
  • xstate-main - node (v18.15.0, x64)
Benchmark Name Iterations
Current pr 6
Baseline baseline 6

Developer Information:

Download Benchmarks

@typescript-bot
Copy link
Collaborator

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

Everything looks good!

@Andarist
Copy link
Contributor Author

Andarist commented Feb 9, 2026

It's kinda a bummer the benchmark numbers don't conclusively show this is an improvement. I'd argue this is like it should have always been unless there is some specific undocumented reason why it shouldn't be. To my best understanding though { kind: 'a' } & { prop: string } should really behave in the same way as { kind: 'a'; prop: string }. This is just an optimization though so the overall behavior stays the same but it's still very much surprising to me that, as a user, I'd pay a penalty like this just because my types were generated dynamically~ using intersections.

FWIW, with the test1 alone this PR leads to:

Strict subtype cache: 0
Subtype cache: 0
Identity cache: 0
-Assignability cache: 58
+Assignability cache: 8
Type Count: 227 -> 228
Instantiation count: 0
Symbol count: 32,634

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

Labels

For Uncommitted Bug PR for untriaged, rejected, closed or missing bug

Projects

Status: Not started

Development

Successfully merging this pull request may close these issues.

3 participants