Skip to content

Fix CFA for with generic T (#62133) #62151

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed

Conversation

kaushik-i-b
Copy link

Fixes

Fixes #62133x && x should not narrow generic T to NonNullable<T>.

What changed

  • Modified getDefinitelyFalsyPartOfType in src/compiler/checker.ts
    • Added a branch for TypeParameter that consults the type’s constraint
    • Added recursion guard to avoid infinite loops

New behaviour

function f<T>(x: T) {
    return x && x;   // now typed as T, not NonNullable<T>
}

@github-project-automation github-project-automation bot moved this to Not started in PR Backlog Jul 30, 2025
@typescript-bot typescript-bot added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Jul 30, 2025
@kaushik-i-b
Copy link
Author

@microsoft-github-policy-service agree

@RyanCavanaugh
Copy link
Member

@typescript-bot test it

@typescript-bot
Copy link
Collaborator

typescript-bot commented Jul 31, 2025

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 @RyanCavanaugh, the results of running the DT tests are ready.

Everything looks the same!

You can check the log here.

@typescript-bot
Copy link
Collaborator

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

Everything looks good!

@typescript-bot
Copy link
Collaborator

@RyanCavanaugh
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 34 34 ~ ~ ~ p=1.000 n=6
Symbols 62,370 62,370 ~ ~ ~ p=1.000 n=6
Types 50,386 50,386 ~ ~ ~ p=1.000 n=6
Memory used 194,192k (± 0.94%) 194,132k (± 0.96%) ~ 192,910k 196,546k p=1.000 n=6
Parse Time 1.31s (± 0.62%) 1.32s (± 0.80%) ~ 1.30s 1.33s p=0.180 n=6
Bind Time 0.73s 0.73s ~ ~ ~ p=1.000 n=6
Check Time 9.75s (± 0.08%) 9.74s (± 0.32%) ~ 9.68s 9.76s p=0.446 n=6
Emit Time 2.74s (± 0.83%) 2.75s (± 0.27%) ~ 2.74s 2.76s p=0.741 n=6
Total Time 14.52s (± 0.19%) 14.53s (± 0.20%) ~ 14.47s 14.55s p=0.744 n=6
angular-1 - node (v18.15.0, x64)
Errors 56 56 ~ ~ ~ p=1.000 n=6
Symbols 948,914 948,914 ~ ~ ~ p=1.000 n=6
Types 410,884 410,884 ~ ~ ~ p=1.000 n=6
Memory used 1,226,424k (± 0.01%) 1,226,352k (± 0.01%) ~ 1,226,278k 1,226,431k p=0.093 n=6
Parse Time 6.52s (± 0.84%) 6.50s (± 0.44%) ~ 6.47s 6.54s p=1.000 n=6
Bind Time 1.87s (± 0.22%) 1.87s (± 0.28%) ~ 1.87s 1.88s p=0.595 n=6
Check Time 31.99s (± 0.37%) 32.01s (± 0.27%) ~ 31.91s 32.15s p=1.000 n=6
Emit Time 14.89s (± 0.19%) 14.83s (± 0.61%) ~ 14.66s 14.89s p=0.166 n=6
Total Time 55.26s (± 0.28%) 55.21s (± 0.28%) ~ 54.95s 55.41s p=0.423 n=6
mui-docs - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 2,543,160 2,543,160 ~ ~ ~ p=1.000 n=6
Types 900,384 900,384 ~ ~ ~ p=1.000 n=6
Memory used 2,829,584k (± 0.01%) 2,829,471k (± 0.01%) ~ 2,829,258k 2,829,674k p=0.230 n=6
Parse Time 8.76s (± 0.38%) 8.76s (± 0.26%) ~ 8.73s 8.79s p=0.936 n=6
Bind Time 2.25s (± 0.52%) 2.26s (± 0.65%) ~ 2.24s 2.28s p=0.142 n=6
Check Time 85.71s (± 0.27%) 85.64s (± 0.40%) ~ 85.35s 86.12s p=0.810 n=6
Emit Time 0.93s (±103.79%) 1.00s (±107.26%) ~ 0.30s 2.41s p=0.797 n=6
Total Time 97.65s (± 1.03%) 97.66s (± 0.94%) ~ 96.69s 98.83s p=1.000 n=6
self-build-src - node (v18.15.0, x64)
Errors 0 2 🔻+2 (+ ∞%) ~ ~ p=0.001 n=6
Symbols 1,227,065 1,301,666 🔻+74,601 (+ 6.08%) ~ ~ p=0.001 n=6
Types 267,478 267,573 +95 (+ 0.04%) ~ ~ p=0.001 n=6
Memory used 2,423,713k (± 6.15%) 2,584,364k (± 5.76%) 🔻+160,652k (+ 6.63%) 2,522,695k 2,888,318k p=0.045 n=6
Parse Time 5.22s (± 0.92%) 6.56s (± 1.08%) 🔻+1.34s (+25.65%) 6.48s 6.65s p=0.005 n=6
Bind Time 1.78s (± 0.90%) 2.42s (± 1.12%) 🔻+0.64s (+35.99%) 2.39s 2.46s p=0.004 n=6
Check Time 35.30s (± 0.51%) 35.27s (± 0.46%) ~ 35.04s 35.54s p=0.575 n=6
Emit Time 3.04s (± 4.61%) 3.00s (± 0.81%) ~ 2.97s 3.03s p=0.936 n=6
Total Time 45.36s (± 0.59%) 47.25s (± 0.29%) 🔻+1.89s (+ 4.18%) 47.06s 47.44s p=0.005 n=6
self-build-src-public-api - node (v18.15.0, x64)
Errors 0 2 🔻+2 (+ ∞%) ~ ~ p=0.001 n=6
Symbols 1,227,065 1,301,666 🔻+74,601 (+ 6.08%) ~ ~ p=0.001 n=6
Types 267,478 267,573 +95 (+ 0.04%) ~ ~ p=0.001 n=6
Memory used 2,919,396k (±12.88%) 2,963,039k (±13.49%) ~ 2,597,311k 3,328,976k p=0.378 n=6
Parse Time 8.49s (± 1.45%) 10.65s (± 1.46%) 🔻+2.16s (+25.48%) 10.46s 10.82s p=0.005 n=6
Bind Time 2.66s (± 1.51%) 3.59s (± 1.36%) 🔻+0.93s (+34.77%) 3.52s 3.63s p=0.005 n=6
Check Time 53.19s (± 0.45%) 53.31s (± 0.38%) ~ 53.09s 53.55s p=0.230 n=6
Emit Time 4.48s (± 3.53%) 4.47s (± 4.18%) ~ 4.24s 4.70s p=0.810 n=6
Total Time 68.82s (± 0.25%) 72.03s (± 0.26%) 🔻+3.21s (+ 4.66%) 71.75s 72.25s p=0.005 n=6
self-compiler - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 262,554 262,555 +1 (+ 0.00%) ~ ~ p=0.001 n=6
Types 107,163 107,163 ~ ~ ~ p=1.000 n=6
Memory used 441,960k (± 0.02%) 441,953k (± 0.02%) ~ 441,856k 442,096k p=0.689 n=6
Parse Time 3.56s (± 0.77%) 3.52s (± 0.95%) ~ 3.47s 3.55s p=0.089 n=6
Bind Time 1.31s (± 0.96%) 1.33s (± 0.95%) +0.02s (+ 1.53%) 1.31s 1.34s p=0.034 n=6
Check Time 18.90s (± 0.38%) 18.93s (± 0.26%) ~ 18.86s 19.00s p=0.810 n=6
Emit Time 1.53s (± 0.87%) 1.52s (± 1.46%) ~ 1.49s 1.55s p=0.566 n=6
Total Time 25.30s (± 0.34%) 25.29s (± 0.24%) ~ 25.20s 25.37s p=0.810 n=6
ts-pre-modules - node (v18.15.0, x64)
Errors 71 71 ~ ~ ~ p=1.000 n=6
Symbols 225,367 225,367 ~ ~ ~ p=1.000 n=6
Types 94,290 94,290 ~ ~ ~ p=1.000 n=6
Memory used 371,172k (± 0.02%) 371,143k (± 0.02%) ~ 371,075k 371,229k p=0.575 n=6
Parse Time 2.88s (± 0.75%) 2.88s (± 1.34%) ~ 2.83s 2.92s p=0.627 n=6
Bind Time 1.59s (± 1.42%) 1.59s (± 0.83%) ~ 1.57s 1.61s p=0.870 n=6
Check Time 16.46s (± 0.31%) 16.43s (± 0.33%) ~ 16.38s 16.52s p=0.227 n=6
Emit Time 0.00s (±244.70%) 0.00s ~ ~ ~ p=0.405 n=6
Total Time 20.93s (± 0.13%) 20.90s (± 0.37%) ~ 20.81s 21.04s p=0.149 n=6
vscode - node (v18.15.0, x64)
Errors 1 1 ~ ~ ~ p=1.000 n=6
Symbols 3,559,337 3,559,337 ~ ~ ~ p=1.000 n=6
Types 1,200,047 1,200,047 ~ ~ ~ p=1.000 n=6
Memory used 3,603,620k (± 0.00%) 3,603,664k (± 0.00%) ~ 3,603,514k 3,603,767k p=0.575 n=6
Parse Time 15.24s (± 0.60%) 15.26s (± 0.70%) ~ 15.16s 15.43s p=0.936 n=6
Bind Time 4.91s (± 0.26%) 4.92s (± 0.61%) ~ 4.89s 4.96s p=0.678 n=6
Check Time 99.86s (± 1.16%) 100.26s (± 1.20%) ~ 99.60s 102.69s p=0.378 n=6
Emit Time 31.75s (± 8.60%) 30.84s (± 0.98%) ~ 30.34s 31.14s p=0.936 n=6
Total Time 151.77s (± 2.16%) 151.28s (± 0.76%) ~ 150.29s 153.54s p=0.689 n=6
webpack - node (v18.15.0, x64)
Errors 2 2 ~ ~ ~ p=1.000 n=6
Symbols 321,136 321,136 ~ ~ ~ p=1.000 n=6
Types 140,173 140,173 ~ ~ ~ p=1.000 n=6
Memory used 477,420k (± 0.03%) 477,487k (± 0.02%) ~ 477,363k 477,606k p=0.575 n=6
Parse Time 4.28s (± 0.57%) 4.27s (± 0.32%) ~ 4.25s 4.28s p=0.285 n=6
Bind Time 1.79s (± 1.24%) 1.80s (± 0.93%) ~ 1.78s 1.82s p=0.512 n=6
Check Time 20.47s (± 0.35%) 20.54s (± 0.39%) ~ 20.46s 20.69s p=0.229 n=6
Emit Time 0.00s (±154.76%) 0.00s (±244.70%) ~ 0.00s 0.01s p=0.595 n=6
Total Time 26.55s (± 0.31%) 26.61s (± 0.32%) ~ 26.54s 26.77s p=0.229 n=6
xstate-main - node (v18.15.0, x64)
Errors 30 30 ~ ~ ~ p=1.000 n=6
Symbols 663,079 663,079 ~ ~ ~ p=1.000 n=6
Types 197,935 197,935 ~ ~ ~ p=1.000 n=6
Memory used 570,047k (± 0.01%) 570,082k (± 0.01%) ~ 569,994k 570,145k p=0.470 n=6
Parse Time 4.28s (± 0.80%) 4.30s (± 0.49%) ~ 4.26s 4.32s p=0.332 n=6
Bind Time 1.33s (± 1.04%) 1.34s (± 1.02%) ~ 1.32s 1.36s p=0.249 n=6
Check Time 20.00s (± 1.52%) 20.00s (± 1.33%) ~ 19.81s 20.52s p=0.689 n=6
Emit Time 0.00s (±244.70%) 0.00s ~ ~ ~ p=0.405 n=6
Total Time 25.61s (± 1.06%) 25.64s (± 1.10%) ~ 25.44s 26.19s p=0.630 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

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

Everything looks good!

@kaushik-i-b
Copy link
Author

kaushik-i-b commented Aug 1, 2025

Will my PR be merged.? 😅

@RyanCavanaugh
Copy link
Member

What's the non-degenerate repro where this matters?

@kaushik-i-b
Copy link
Author

kaushik-i-b commented Aug 1, 2025

I pushed commit that removes the new test file and instead adds a minimal, non-degenerate test into an existing file:

  • New case:
// Generic type parameter keeps falsy constituents (#62133)
declare function id<T>(x: T): T;
function f<T extends string | null>(x: T) {
    const y = x && x;   // should be T, not NonNullable<T>
    return y;
}
const t1 = f("a");   // "a"
const t2 = f(null);  // null  (was incorrectly never)

The baseline diff now shows only the expected new lines, and the
change is exercised without a dedicated file.

@RyanCavanaugh
Copy link
Member

There's no reason to write x && x, though. What's code that you would write on purpose that doesn't work without this PR?

@kaushik-i-b
Copy link
Author

There's no reason to write x && x, though. What's code that you would write on purpose that doesn't work without this PR?

Sorry for the confusion...😅
Code fix explained in one sentence
We taught getDefinitelyFalsyPartOfType to look at the constraint of a generic type parameter instead of immediately returning never, so CFA no longer narrows T && anything to NonNullable<T>.


What the old code did

if (type.flags & TypeFlags.TypeParameter) {
    return neverType;   // “the falsy part of T is nothing”
}

Result: in control-flow analysis the expression left && right is calculated as
left minus nothingleft becomes NonNullable<T>.


What the new code does

if (type.flags & TypeFlags.TypeParameter) {
    const constraint = getBaseConstraintOfType(type);
    if (!constraint || constraint === type) return neverType;
    return getDefinitelyFalsyPartOfType(constraint);
}
  • If T extends string | null, its constraint is string | null;
  • getDefinitelyFalsyPartOfType(string | null) correctly returns null;
  • CFA now subtracts only null from T, leaving the original union intact.
    No NonNullable narrowing, no runtime surprises.

@RyanCavanaugh
Copy link
Member

What user code is affected by this in a situation where the code makes sense in the first place? How can I observe if this PR is present or not without writing nonsense code like x && x ?

If there's not an answer to that question, I don't think we should merge this -- code changes need to be well-motivated by actual examples.

@kaushik-i-b
Copy link
Author

What user code is affected by this in a situation where the code makes sense in the first place? How can I observe if this PR is present or not without writing nonsense code like x && x ?

If there's not an answer to that question, I don't think we should merge this -- code changes need to be well-motivated by actual examples.

After the discussion above I agree the real-world impact is too small to justify the extra complexity. I’m closing the PR—thanks for the thorough review!
Thank you

@kaushik-i-b kaushik-i-b closed this Aug 1, 2025
@github-project-automation github-project-automation bot moved this from Not started to Done in PR Backlog Aug 1, 2025
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: Done
Development

Successfully merging this pull request may close these issues.

x && x should not be NonNullable
3 participants