Skip to content
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

Let properties having primitive types discriminate object unions #60718

Open
wants to merge 28 commits into
base: main
Choose a base branch
from

Conversation

jfet97
Copy link
Contributor

@jfet97 jfet97 commented Dec 9, 2024

Main goal

The ultimate goal of this PR is to allow properties having primitive types discriminate object unions, even when there are no unit/literal types involved. Some examples of this are as follows:

interface State<Type> {
  state: Type;
}

interface UserName {
  first: string;
  last?: string;
}

const nameState = {} as {
  value: string;
  state: State<string>;
} | {
  value: UserName;
  state: State<UserName>;
}

// here 'value' is considered a discriminant property
if (typeof nameState.value === "string") {
  nameState.state satisfies State<string>;
} else {
  nameState.state satisfies State<UserName>;
}
declare const arr: [string, string] | [number, number];

// here 'arr[0]' is considered a discriminant property
if (typeof arr[0] === "string") {
  arr[1] satisfies string;
} else {
  arr[0] satisfies number;
  arr[1] satisfies number;
}
type ToRet<T extends { a: string } | { a: number }> =
  T extends { a: string }
    ? string
    : T extends { a: number }
      ? number
      : never

// here by narrowing on 'a' we can type this function, thanks the new capabilities from PR #56941
function aStringOrANumber<T extends { a: string } | { a: number }>(param: T): ToRet<T> {
  if (typeof param.a === "string") {
    return param.a.repeat(3);
  }
  if (typeof param.a === "number") {
    return Math.exp(param.a);
  }
  throw new Error()
}

If a property was not previously considered discriminant, it will now be treated as one when the following conditions are met:

  1. The property must be non-uniform; specifically, at least two constituents of the union must have it with distinct types.
  2. The property must include at least one primitive type as a possible type.

 

Implementation details

isDiscriminantProperty were a cached boolean in the links of transient symbols, so the first attempt consisted of just adding inside the isDiscriminantProperty function (prop.links.checkFlags & CheckFlags.HasNonUniformType) && someType(propType, t => !!(t.flags & TypeFlags.Primitive)) as a fallback.

Unfortunately, I found a couple of problems with this.

The first one is related with tests/cases/conformance/types/union/contextualTypeWithUnionTypeObjectLiteral.ts:

var objStrOrNum3: { prop: string } | { prop: number } = {
    prop: strOrNumber
};

The above assignment must fail (you can find the reason in that conformance test), but considering prop as a discriminant field changed the behaviour of TS, ultimately allowing the assignment. I ended up disabling the new discriminativeness of prop in similar situations and the simpler solution was by using a flag, which I had to take into consideration when it comes to caching the final result. That test case is 10 years old, that behaviour is literally set in stone, and I don't have the guts to change it, even though the change would be more permissive so it shouldn't be a breaking change, hopefully.

The second problem is related to #60702. Enabling the discriminativness on a particular prop makes objects that declared that property as never disappear when narrowing. TLDR TS stopped compiling itself here because of how ArrowFunction is defined: its name property has never type so it disappeared as a possible type of node after the assertion. Therefore I changed its type from : never to ?: never as suggested by Ryan, both in src/compiler/types, where the problematic ArrowFunction lies, and in tests/baselines/reference/api/typescript.d.ts.

I don’t think this last change is problematic; rather, I think a possible indirect breaking change might lie in the fact that if a property shared between objects is typed as never in one or more cases and is now considered discriminant due to this additional discriminating capability of TS, then the type containing it could end up being eliminated by TS in various circumstances. I’d like to point out that this already happens when a property is considered discriminant, as can be seen in the linked issue. Simply put, enhancing TS’s capabilities in this regard increases the cases where it can occur.

 
Edit: The implementation has changed a bit. I ended up disabling the new capability everywhere except where it was really needed, so now that flag is false by default. Furthermore, to check if a property has at least one primitive type I added one specific CheckFlags that createUnionOrIntersectionProperty will set when is appropriate. More info here.

 

Final considerations

I wonder if isDiscriminantWithNeverType should take into account what’s been added by this PR. I’m quite unsure about it.

@typescript-bot typescript-bot added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Dec 9, 2024
@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.

@typescript-bot
Copy link
Collaborator

Looks like you're introducing a change to the public API surface area. If this includes breaking changes, please document them on our wiki's API Breaking Changes page.

Also, please make sure @DanielRosenwasser and @RyanCavanaugh are aware of the changes, just as a heads up.

@jfet97 jfet97 changed the title Let primitive types discriminate a union of objects cleaned Let primitive types discriminate a union of objects Dec 9, 2024
@jakebailey
Copy link
Member

@typescript-bot test it

@typescript-bot
Copy link
Collaborator

typescript-bot commented Dec 9, 2024

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, it looks like the DT test run failed. Please check the log for more details.

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/60718/merge:

There were infrastructure failures potentially unrelated to your change:

  • 1 instance of "Package install failed"

Otherwise...

Everything looks good!

@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
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,363 62,363 ~ ~ ~ p=1.000 n=6
Types 50,395 50,401 +6 (+ 0.01%) ~ ~ p=0.001 n=6
Memory used 193,749k (± 0.74%) 193,882k (± 0.73%) ~ 193,131k 196,746k p=0.298 n=6
Parse Time 1.31s (± 0.94%) 1.30s (± 0.80%) ~ 1.29s 1.32s p=1.000 n=6
Bind Time 0.72s 0.72s ~ ~ ~ p=1.000 n=6
Check Time 9.77s (± 0.82%) 9.78s (± 0.56%) ~ 9.71s 9.85s p=1.000 n=6
Emit Time 2.73s (± 0.55%) 2.73s (± 0.86%) ~ 2.70s 2.75s p=0.684 n=6
Total Time 14.53s (± 0.52%) 14.53s (± 0.37%) ~ 14.47s 14.60s p=0.810 n=6
angular-1 - node (v18.15.0, x64)
Errors 37 37 ~ ~ ~ p=1.000 n=6
Symbols 947,936 947,937 +1 (+ 0.00%) ~ ~ p=0.001 n=6
Types 410,955 411,052 +97 (+ 0.02%) ~ ~ p=0.001 n=6
Memory used 1,225,814k (± 0.00%) 1,226,403k (± 0.00%) +589k (+ 0.05%) 1,226,334k 1,226,491k p=0.005 n=6
Parse Time 6.69s (± 0.62%) 6.63s (± 0.57%) -0.06s (- 0.87%) 6.60s 6.70s p=0.023 n=6
Bind Time 1.89s (± 0.33%) 1.89s (± 0.33%) ~ 1.88s 1.90s p=1.000 n=6
Check Time 31.93s (± 0.36%) 32.01s (± 0.19%) ~ 31.92s 32.10s p=0.258 n=6
Emit Time 15.16s (± 0.30%) 15.21s (± 0.39%) ~ 15.13s 15.29s p=0.195 n=6
Total Time 55.66s (± 0.21%) 55.73s (± 0.17%) ~ 55.63s 55.87s p=0.261 n=6
mui-docs - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 2,447,028 2,447,040 +12 (+ 0.00%) ~ ~ p=0.001 n=6
Types 896,160 896,442 +282 (+ 0.03%) ~ ~ p=0.001 n=6
Memory used 2,318,441k (± 0.00%) 2,326,551k (± 0.00%) +8,110k (+ 0.35%) 2,326,487k 2,326,600k p=0.005 n=6
Parse Time 9.41s (± 0.19%) 9.42s (± 0.31%) ~ 9.38s 9.44s p=0.367 n=6
Bind Time 2.23s (± 0.40%) 2.23s (± 0.47%) ~ 2.21s 2.24s p=0.452 n=6
Check Time 73.24s (± 0.44%) 73.60s (± 0.38%) ~ 73.19s 74.06s p=0.066 n=6
Emit Time 0.29s (± 2.61%) 0.28s (± 1.82%) ~ 0.28s 0.29s p=0.247 n=6
Total Time 85.17s (± 0.40%) 85.52s (± 0.32%) ~ 85.15s 85.99s p=0.065 n=6
self-build-src - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 1,225,328 1,225,284 -44 (- 0.00%) ~ ~ p=0.001 n=6
Types 266,569 266,626 +57 (+ 0.02%) ~ ~ p=0.001 n=6
Memory used 2,414,589k (± 6.13%) 2,415,407k (± 6.15%) ~ 2,353,909k 2,719,150k p=0.230 n=6
Parse Time 5.23s (± 0.93%) 5.23s (± 1.21%) ~ 5.16s 5.33s p=0.689 n=6
Bind Time 1.77s (± 1.11%) 1.76s (± 1.23%) ~ 1.74s 1.80s p=0.288 n=6
Check Time 35.10s (± 0.65%) 35.36s (± 0.65%) ~ 35.03s 35.70s p=0.128 n=6
Emit Time 3.00s (± 2.90%) 2.97s (± 0.76%) ~ 2.94s 2.99s p=0.688 n=6
Total Time 45.11s (± 0.44%) 45.34s (± 0.49%) ~ 45.05s 45.68s p=0.109 n=6
self-build-src-public-api - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 1,225,328 1,225,284 -44 (- 0.00%) ~ ~ p=0.001 n=6
Types 266,569 266,626 +57 (+ 0.02%) ~ ~ p=0.001 n=6
Memory used 2,788,094k (±14.24%) 3,151,710k (± 0.03%) 🔻+363,616k (+13.04%) 3,150,622k 3,152,685k p=0.031 n=6
Parse Time 6.97s (± 1.66%) 7.03s (± 0.79%) ~ 6.95s 7.09s p=0.471 n=6
Bind Time 2.16s (± 2.47%) 2.14s (± 2.06%) ~ 2.10s 2.20s p=0.519 n=6
Check Time 42.75s (± 0.47%) 42.94s (± 0.17%) ~ 42.86s 43.04s p=0.093 n=6
Emit Time 3.54s (± 6.27%) 3.49s (± 2.33%) ~ 3.37s 3.58s p=0.810 n=6
Total Time 55.40s (± 0.56%) 55.60s (± 0.29%) ~ 55.39s 55.85s p=0.471 n=6
self-compiler - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 262,267 262,216 -51 (- 0.02%) ~ ~ p=0.001 n=6
Types 106,628 106,674 +46 (+ 0.04%) ~ ~ p=0.001 n=6
Memory used 439,909k (± 0.01%) 440,319k (± 0.03%) +409k (+ 0.09%) 440,204k 440,489k p=0.005 n=6
Parse Time 3.52s (± 1.71%) 3.55s (± 0.74%) ~ 3.50s 3.57s p=0.107 n=6
Bind Time 1.32s (± 0.78%) 1.31s (± 0.96%) ~ 1.30s 1.33s p=0.359 n=6
Check Time 18.91s (± 0.53%) 19.05s (± 0.25%) +0.14s (+ 0.75%) 18.98s 19.10s p=0.016 n=6
Emit Time 1.53s (± 1.06%) 1.54s (± 1.19%) ~ 1.51s 1.56s p=0.622 n=6
Total Time 25.28s (± 0.49%) 25.45s (± 0.21%) +0.16s (+ 0.65%) 25.35s 25.50s p=0.013 n=6
ts-pre-modules - node (v18.15.0, x64)
Errors 70 70 ~ ~ ~ p=1.000 n=6
Symbols 226,062 226,071 +9 (+ 0.00%) ~ ~ p=0.001 n=6
Types 94,488 94,521 +33 (+ 0.03%) ~ ~ p=0.001 n=6
Memory used 371,593k (± 0.02%) 371,942k (± 0.01%) +349k (+ 0.09%) 371,874k 371,983k p=0.005 n=6
Parse Time 2.91s (± 1.39%) 2.91s (± 1.20%) ~ 2.85s 2.95s p=1.000 n=6
Bind Time 1.58s (± 0.66%) 1.59s (± 0.97%) ~ 1.57s 1.61s p=0.621 n=6
Check Time 16.54s (± 0.44%) 16.53s (± 0.17%) ~ 16.48s 16.56s p=0.810 n=6
Emit Time 0.00s 0.00s ~ ~ ~ p=1.000 n=6
Total Time 21.03s (± 0.43%) 21.03s (± 0.14%) ~ 20.98s 21.06s p=0.744 n=6
vscode - node (v18.15.0, x64)
Errors 3 4 🔻+1 (+33.33%) ~ ~ p=0.001 n=6
Symbols 3,197,783 3,197,786 +3 (+ 0.00%) ~ ~ p=0.001 n=6
Types 1,099,256 1,099,301 +45 (+ 0.00%) ~ ~ p=0.001 n=6
Memory used 3,271,590k (± 0.01%) 3,273,404k (± 0.01%) +1,813k (+ 0.06%) 3,273,104k 3,274,285k p=0.005 n=6
Parse Time 14.19s (± 0.27%) 14.16s (± 0.27%) -0.04s (- 0.27%) 14.12s 14.23s p=0.044 n=6
Bind Time 4.51s (± 0.52%) 4.58s (± 2.37%) ~ 4.50s 4.80s p=0.089 n=6
Check Time 88.05s (± 2.43%) 87.44s (± 1.90%) ~ 86.57s 90.82s p=1.000 n=6
Emit Time 28.25s (± 2.51%) 27.71s (± 8.59%) ~ 22.86s 28.88s p=0.689 n=6
Total Time 135.00s (± 1.12%) 133.90s (± 0.60%) ~ 132.36s 134.54s p=0.378 n=6
webpack - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 288,747 288,747 ~ ~ ~ p=1.000 n=6
Types 117,158 117,160 +2 (+ 0.00%) ~ ~ p=0.001 n=6
Memory used 440,991k (± 0.05%) 441,029k (± 0.02%) ~ 440,915k 441,160k p=0.093 n=6
Parse Time 4.08s (± 1.40%) 4.10s (± 1.49%) ~ 4.04s 4.18s p=0.630 n=6
Bind Time 1.76s (± 2.07%) 1.77s (± 0.75%) ~ 1.75s 1.79s p=0.808 n=6
Check Time 18.84s (± 0.65%) 18.83s (± 0.21%) ~ 18.77s 18.89s p=1.000 n=6
Emit Time 0.00s 0.00s ~ ~ ~ p=1.000 n=6
Total Time 24.69s (± 0.64%) 24.70s (± 0.30%) ~ 24.59s 24.79s p=0.521 n=6
xstate-main - node (v18.15.0, x64)
Errors 5 5 ~ ~ ~ p=1.000 n=6
Symbols 552,389 552,336 -53 (- 0.01%) ~ ~ p=0.001 n=6
Types 185,093 185,093 ~ ~ ~ p=1.000 n=6
Memory used 492,534k (± 0.01%) 493,056k (± 0.01%) +522k (+ 0.11%) 493,019k 493,110k p=0.005 n=6
Parse Time 3.44s (± 0.63%) 3.41s (± 0.82%) ~ 3.38s 3.45s p=0.198 n=6
Bind Time 1.18s (± 0.93%) 1.16s (± 0.77%) -0.02s (- 1.69%) 1.15s 1.17s p=0.018 n=6
Check Time 19.49s (± 0.37%) 19.48s (± 0.31%) ~ 19.42s 19.59s p=0.873 n=6
Emit Time 0.00s 0.00s ~ ~ ~ p=1.000 n=6
Total Time 24.11s (± 0.37%) 24.05s (± 0.29%) ~ 23.96s 24.17s p=0.421 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

@jakebailey
Copy link
Member

The perf test indicates an extra error in the vscode codebase, which should be looked at.

@jfet97
Copy link
Contributor Author

jfet97 commented Dec 13, 2024

Follow up about the need of the considerNonUniformPrimitivePropDiscriminant flag to disable this new thing in assignments. Could we just accept the following three? They come from tests/cases/conformance/types/union/contextualTypeWithUnionTypeObjectLiteral.ts.

var objStrOrNum3: { prop: string } | { prop: number } = {
    prop: strOrNumber
};

var objStrOrNum6: { prop: string; anotherP: string; } | { prop: number } = {
    prop: strOrNumber,
    anotherP: str
};

var objStrOrNum8: { prop: string; anotherP: string; } | { prop: number; anotherP1: number } = {
    prop: strOrNumber,
    anotherP: str,
    anotherP1: num
};

They are all sound if you think about it, maybe just a little bit controversial. We could remove that flag and the implementation of this PR would be cleaner and probably a little bit faster as well.

Edit: Super seeded by the following changes.

isDiscriminant = (((transientSymbol.links.checkFlags & CheckFlags.Discriminant) === CheckFlags.Discriminant)
|| !!(considerNonUniformPrimitivePropDiscriminant && (transientSymbol.links.checkFlags & CheckFlags.HasNonUniformType) && someType(propType, t => !!(t.flags & TypeFlags.Primitive))))
&& !isGenericType(propType);

Copy link
Member

Choose a reason for hiding this comment

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

This check can probably be done in createUnionOrIntersectionProperty, setting a new CheckFlag.HasPrimitive, like we do for HasNonUniformType.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@gabritto Yes @Andarist had already planted the idea in my mind, and I had considered this possibility in case there were performance issues. Do you think I should implement it in any case?

Copy link
Member

Choose a reason for hiding this comment

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

Even if it doesn't matter for the perf benchmarks we have, I think yes, to be consistent and keep similar code in a single place.

Copy link
Contributor Author

@jfet97 jfet97 Dec 16, 2024

Choose a reason for hiding this comment

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

@gabritto some feedback.

The basic change is quite easy, but there are some problems because createUnionOrIntersectionProperty terminates with result.links.type = isUnion ? getUnionType(propTypes) : getIntersectionType(propTypes), and is this resulting type the one on which I'm currently running someType(propType, t => !!(t.flags & TypeFlags.Primitive))). It's like it has already been reduced/cleaned up/whatever.

Just setting to 1 the CheckFlag.HasPrimitiveType beforehand like we do for other flags doesn't seem a good idea. I got 8 failing baselines and some changes are pretty weird. I'd suspect the right condition for setting the HasPrimitiveType flag will be a little convoluted. Or will be convoluted the one inside isDiscriminantProperty.

I pushed the current changes so you can have a look and maybe spot some stupid error I'm doing, or provide some insights for the above conditions.

Copy link
Contributor Author

@jfet97 jfet97 Dec 16, 2024

Choose a reason for hiding this comment

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

@gabritto plot twist, I disabled the new considerNonUniformPrimitivePropDiscriminant discriminating flag everywhere but in the only place I was sure we need it and things have improved a lot. I like it! The new discriminating power is being activated in more cases with your suggestion, that's why I changed more baselines, but the changes seemed to be correct to me. Please re-check them.

I still have to check why the binder.ts stopped compiling tho...

Copy link
Contributor Author

@jfet97 jfet97 Dec 16, 2024

Choose a reason for hiding this comment

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

Pushed a fix that you may dislike, so I'm open to suggestions.

The problem:

node.body // has type body?: ModuleBody | undefined

The condition node.body is checked so its type is refined to just ModuleBody. That's fine!

Then you check !node.body.parent where:

node.body.parent // has type ModuleDeclaration | Node | ModuleBody | SourceFile

It cannot be non-defined! So the check !node.body.parent now narrows the type of node.body from ModuleBody to never.

Now, this is happening when building that structure so it's perfectly possible that the parent is not set yet. And I understand why you may not want to reflect this into types. Probably for all other intents and purposes that field will be always set. But TS just see the types, and with more refinement capabilities it now understands that the !node.body.parent case should be impossible.

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 PR is turning out to be more and more overzealous; I can already see a ton of projects in the top 400 breaking even worse.

@gabritto
Copy link
Member

@typescript-bot test it

@typescript-bot
Copy link
Collaborator

typescript-bot commented Dec 16, 2024

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

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

Everything looks good!

@typescript-bot
Copy link
Collaborator

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

Everything looks the same!

You can check the log here.

@typescript-bot
Copy link
Collaborator

@gabritto
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,363 62,475 +112 (+ 0.18%) ~ ~ p=0.001 n=6
Types 50,395 50,413 +18 (+ 0.04%) ~ ~ p=0.001 n=6
Memory used 196,694k (± 0.06%) 195,823k (± 0.98%) ~ 193,303k 197,254k p=0.378 n=6
Parse Time 1.61s (± 1.28%) 1.60s (± 1.37%) ~ 1.57s 1.63s p=0.571 n=6
Bind Time 0.88s (± 1.24%) 0.87s (± 0.87%) ~ 0.86s 0.88s p=0.084 n=6
Check Time 11.80s (± 0.87%) 11.89s (± 0.93%) ~ 11.76s 12.05s p=0.261 n=6
Emit Time 3.38s (± 5.01%) 3.50s (± 2.31%) ~ 3.34s 3.56s p=0.335 n=6
Total Time 17.67s (± 0.71%) 17.86s (± 0.82%) +0.19s (+ 1.08%) 17.60s 18.05s p=0.037 n=6
angular-1 - node (v18.15.0, x64)
Errors 37 37 ~ ~ ~ p=1.000 n=6
Symbols 947,936 947,937 +1 (+ 0.00%) ~ ~ p=0.001 n=6
Types 410,955 410,977 +22 (+ 0.01%) ~ ~ p=0.001 n=6
Memory used 1,226,008k (± 0.01%) 1,226,585k (± 0.00%) +577k (+ 0.05%) 1,226,518k 1,226,639k p=0.005 n=6
Parse Time 8.12s (± 0.60%) 8.08s (± 0.66%) ~ 8.00s 8.15s p=0.170 n=6
Bind Time 2.29s (± 0.99%) 2.29s (± 0.45%) ~ 2.27s 2.30s p=1.000 n=6
Check Time 38.17s (± 0.40%) 38.29s (± 0.57%) ~ 38.00s 38.62s p=0.335 n=6
Emit Time 18.29s (± 0.26%) 18.33s (± 0.32%) ~ 18.23s 18.40s p=0.148 n=6
Total Time 66.86s (± 0.31%) 66.98s (± 0.36%) ~ 66.60s 67.25s p=0.378 n=6
mui-docs - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 2,448,353 2,448,353 ~ ~ ~ p=1.000 n=6
Types 896,130 896,404 +274 (+ 0.03%) ~ ~ p=0.001 n=6
Memory used 2,320,597k (± 0.01%) 2,328,632k (± 0.01%) +8,035k (+ 0.35%) 2,328,353k 2,328,773k p=0.005 n=6
Parse Time 11.31s (± 0.82%) 11.35s (± 0.32%) ~ 11.30s 11.39s p=0.745 n=6
Bind Time 2.67s (± 0.31%) 2.68s (± 0.68%) ~ 2.66s 2.71s p=0.236 n=6
Check Time 89.00s (± 1.87%) 88.56s (± 0.96%) ~ 87.85s 90.04s p=1.000 n=6
Emit Time 0.35s (± 2.82%) 0.65s (±113.02%) ~ 0.34s 2.14s p=0.673 n=6
Total Time 103.33s (± 1.58%) 103.24s (± 1.07%) ~ 102.23s 104.67s p=1.000 n=6
self-build-src - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 1,225,390 1,225,510 +120 (+ 0.01%) ~ ~ p=0.001 n=6
Types 266,584 266,701 +117 (+ 0.04%) ~ ~ p=0.001 n=6
Memory used 2,964,189k (±10.04%) 2,723,934k (±14.66%) ~ 2,358,447k 3,088,963k p=0.689 n=6
Parse Time 6.75s (± 1.67%) 6.70s (± 1.77%) ~ 6.50s 6.84s p=0.575 n=6
Bind Time 2.13s (± 2.14%) 2.13s (± 2.27%) ~ 2.05s 2.18s p=0.747 n=6
Check Time 42.75s (± 0.40%) 43.45s (± 0.74%) +0.71s (+ 1.65%) 43.12s 43.93s p=0.005 n=6
Emit Time 3.41s (± 2.72%) 3.61s (± 4.19%) 🔻+0.20s (+ 5.82%) 3.44s 3.80s p=0.020 n=6
Total Time 55.02s (± 0.36%) 55.89s (± 0.54%) +0.88s (+ 1.59%) 55.50s 56.34s p=0.005 n=6
self-build-src-public-api - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 1,225,390 1,225,510 +120 (+ 0.01%) ~ ~ p=0.001 n=6
Types 266,584 266,701 +117 (+ 0.04%) ~ ~ p=0.001 n=6
Memory used 3,029,161k (± 9.77%) 2,912,209k (±12.86%) ~ 2,427,644k 3,155,445k p=0.230 n=6
Parse Time 7.00s (± 1.00%) 6.94s (± 1.88%) ~ 6.75s 7.07s p=0.689 n=6
Bind Time 2.14s (± 2.46%) 2.15s (± 2.10%) ~ 2.11s 2.22s p=0.936 n=6
Check Time 42.79s (± 0.45%) 43.31s (± 0.67%) +0.52s (+ 1.22%) 42.82s 43.55s p=0.020 n=6
Emit Time 3.53s (± 3.18%) 3.50s (± 2.33%) ~ 3.39s 3.62s p=0.936 n=6
Total Time 55.47s (± 0.56%) 55.91s (± 0.75%) ~ 55.14s 56.25s p=0.078 n=6
self-compiler - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 262,278 262,309 +31 (+ 0.01%) ~ ~ p=0.001 n=6
Types 106,628 106,689 +61 (+ 0.06%) ~ ~ p=0.001 n=6
Memory used 439,888k (± 0.01%) 440,961k (± 0.01%) +1,073k (+ 0.24%) 440,861k 441,018k p=0.005 n=6
Parse Time 3.52s (± 0.84%) 3.54s (± 1.11%) ~ 3.48s 3.58s p=0.148 n=6
Bind Time 1.32s (± 1.14%) 1.32s (± 1.23%) ~ 1.29s 1.33s p=0.209 n=6
Check Time 18.92s (± 0.38%) 19.28s (± 0.36%) +0.36s (+ 1.92%) 19.19s 19.37s p=0.005 n=6
Emit Time 1.53s (± 0.96%) 1.52s (± 1.05%) ~ 1.50s 1.54s p=0.511 n=6
Total Time 25.28s (± 0.30%) 25.67s (± 0.17%) +0.39s (+ 1.55%) 25.62s 25.74s p=0.005 n=6
ts-pre-modules - node (v18.15.0, x64)
Errors 70 71 +1 (+ 1.43%) ~ ~ p=0.001 n=6
Symbols 226,062 226,095 +33 (+ 0.01%) ~ ~ p=0.001 n=6
Types 94,488 94,530 +42 (+ 0.04%) ~ ~ p=0.001 n=6
Memory used 371,601k (± 0.01%) 372,075k (± 0.01%) +474k (+ 0.13%) 372,032k 372,122k p=0.005 n=6
Parse Time 2.91s (± 1.13%) 2.91s (± 0.87%) ~ 2.87s 2.94s p=0.935 n=6
Bind Time 1.59s (± 0.76%) 1.60s (± 1.12%) ~ 1.58s 1.62s p=0.198 n=6
Check Time 16.50s (± 0.19%) 16.89s (± 0.17%) +0.39s (+ 2.36%) 16.85s 16.92s p=0.005 n=6
Emit Time 0.00s 0.00s ~ ~ ~ p=1.000 n=6
Total Time 21.00s (± 0.19%) 21.40s (± 0.15%) +0.40s (+ 1.92%) 21.36s 21.44s p=0.005 n=6
vscode - node (v18.15.0, x64)
Errors 3 25 🔻+22 (+733.33%) ~ ~ p=0.001 n=6
Symbols 3,220,155 3,220,121 -34 (- 0.00%) ~ ~ p=0.001 n=6
Types 1,107,836 1,107,895 +59 (+ 0.01%) ~ ~ p=0.001 n=6
Memory used 3,286,533k (± 0.01%) 3,288,660k (± 0.01%) +2,127k (+ 0.06%) 3,288,209k 3,289,091k p=0.005 n=6
Parse Time 14.15s (± 0.34%) 14.12s (± 0.65%) ~ 14.02s 14.26s p=0.573 n=6
Bind Time 4.54s (± 0.30%) 4.52s (± 0.33%) ~ 4.50s 4.54s p=0.134 n=6
Check Time 88.65s (± 1.78%) 88.62s (± 2.13%) ~ 86.07s 90.67s p=1.000 n=6
Emit Time 28.15s (± 2.35%) 27.91s (± 3.02%) ~ 27.31s 29.33s p=0.521 n=6
Total Time 135.48s (± 0.77%) 135.18s (± 1.66%) ~ 132.29s 138.71s p=0.810 n=6
webpack - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 291,463 291,463 ~ ~ ~ p=1.000 n=6
Types 118,920 118,921 +1 (+ 0.00%) ~ ~ p=0.001 n=6
Memory used 445,230k (± 0.02%) 445,320k (± 0.03%) ~ 445,045k 445,461k p=0.297 n=6
Parse Time 4.12s (± 0.73%) 4.10s (± 1.05%) ~ 4.04s 4.15s p=0.335 n=6
Bind Time 1.78s (± 1.42%) 1.78s (± 1.54%) ~ 1.74s 1.81s p=1.000 n=6
Check Time 18.81s (± 0.50%) 18.84s (± 0.63%) ~ 18.68s 18.95s p=0.521 n=6
Emit Time 0.00s 0.00s ~ ~ ~ p=1.000 n=6
Total Time 24.71s (± 0.33%) 24.71s (± 0.59%) ~ 24.46s 24.86s p=1.000 n=6
xstate-main - node (v18.15.0, x64)
Errors 5 5 ~ ~ ~ p=1.000 n=6
Symbols 552,233 552,233 ~ ~ ~ p=1.000 n=6
Types 184,971 184,977 +6 (+ 0.00%) ~ ~ p=0.001 n=6
Memory used 492,247k (± 0.01%) 492,796k (± 0.01%) +549k (+ 0.11%) 492,735k 492,887k p=0.005 n=6
Parse Time 2.76s (± 0.27%) 2.76s (± 0.20%) ~ 2.75s 2.76s p=0.476 n=6
Bind Time 0.96s 0.96s ~ ~ ~ p=1.000 n=6
Check Time 16.19s (± 0.29%) 16.21s (± 0.38%) ~ 16.13s 16.32s p=0.686 n=6
Emit Time 0.00s 0.00s ~ ~ ~ p=1.000 n=6
Total Time 19.91s (± 0.25%) 19.93s (± 0.33%) ~ 19.84s 20.04s p=0.747 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

@jfet97
Copy link
Contributor Author

jfet97 commented Dec 17, 2024

22 new errors just in vscode. This will be fun

@typescript-bot
Copy link
Collaborator

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

Something interesting changed - please have a look.

Details

compiler-explorer/compiler-explorer

3 of 6 projects failed to build with the old tsc and were ignored

tsconfig.tests.json

tsconfig.json

continuedev/continue

6 of 13 projects failed to build with the old tsc and were ignored

extensions/vscode/tsconfig.json

core/tsconfig.npm.json

core/tsconfig.json

binary/tsconfig.json

gui/tsconfig.json

desktop/desktop

1 of 5 projects failed to build with the old tsc and were ignored

tsconfig.json

ether/etherpad-lite

src/tsconfig.json

google/blockly

2 of 3 projects failed to build with the old tsc and were ignored

tsconfig.json

ionic-team/ionic-framework

23 of 24 projects failed to build with the old tsc and were ignored

core/tsconfig.json

microsoft/vscode

5 of 55 projects failed to build with the old tsc and were ignored

src/tsconfig.tsec.json

src/tsconfig.monaco.json

Redocly/redoc

tsconfig.lib.json

tsconfig.json

@jfet97
Copy link
Contributor Author

jfet97 commented Dec 17, 2024

compiler-explorer/compiler-explorer

if (currentLabel !== null && currentLabel.initialLine === undefined) {
    currentLabel.initialLine = ln;
}

currentLabel has type Label | null. If it is not null then currentLabel.initialLine has type number. It cannot be undefined! TS now understands this impossible situation and types currentLabel as never inside the if.

 

continuedev/continue

In addition to this, this, this and this. Please note they do not set strict: true and have strictNullChecks disabled.

A PackageDocsResult is defined as:

type PackageDocsResult = {
  packageInfo: ParsedPackageInfo;
} & (
  | { error:  string; details?: never }
  | { details: PackageDetailsSuccess; error?: never }
);

In AddDocsDialog they also do:

if (docsResult.error || !docsResult.details) {
  // ...
  return
}

docsResult // used after but has type 'never' here

It seems the above if makes the { details: PackageDetailsSuccess; error?: never } case disappear when checking docsResult.error. It seems just another instance of #60702. Typing properties as never, even as ?: never when strictNullChecks: false, seems to be a really bad idea.

When it comes to CompletionProvider.ts, they do:

// Temporary fix for JetBrains autocomplete bug as described in https://github.com/continuedev/continue/pull/3022
if (llm.model === undefined && llm.completionOptions?.model !== undefined) {
  llm.model = llm.completionOptions.model;
}

But llm.model has type string. It cannot be undefined! So guess who becomes never...

@jfet97
Copy link
Contributor Author

jfet97 commented Dec 17, 2024

desktop/desktop

About app/src/ui/app.tsx:

enum DragType {
  Commit,
}

switch (currentDragElement.type) {
  case DragType.Commit:
    return (
      <CommitDragElement
        gitHubRepository={gitHubRepository}
        commit={commit}
        selectedCommits={selectedCommits}
        emoji={emoji}
        accounts={this.state.accounts}
      />
    )
  default:
    return assertNever(
      currentDragElement.type,
      `Unknown drag element type: ${currentDragElement}`
    )
}

It seems they do not need that assertNever anymore 🙄.

When it comes to app/src/ui/test-notifications/test-notifications.tsx:

TestNotificationType {
  PullRequestReview,
  PullRequestComment,
  ChecksFailed,
}

switch (selectedFlow.type) {
  case TestNotificationType.PullRequestReview: {
    // ...
    break
  }
  case TestNotificationType.PullRequestComment: {
    // ...
    break
  }
  case TestNotificationType.ChecksFailed: {
    // ...
    break
  }
  default:
    assertNever(selectedFlow.type, `Unknown flow type: ${selectedFlow}`)
}

Same thing!
Here is a possible solution.

 

ether/etherpad-lite

Nothing new! Same as here.

@jfet97
Copy link
Contributor Author

jfet97 commented Dec 17, 2024

google/blockly

if (typeof label === 'string') {
  return [parsing.replaceMessageReferences(label), value];
}

hasImages = true;

const imageLabel =
  label.alt !== null
    ? { ...label, alt: parsing.replaceMessageReferences(label.alt) }
    : { ...label }; // <-- new error here

The alt property has type string. It cannot be null! So label is typed as never in the false branch of the ternary. This is a consequence of having narrowed label few lines before from string | ImageProperties to ImageProperties.

 

ionic-team/ionic-framework

if (data.year !== undefined) {
  // ...
} else if (data.hour !== undefined) { // <-- new error here
  rtn = twoDigit(data.hour) + ':' + twoDigit(data.minute); // <-- new errors here
}

We have that data was previously narrowed from DatetimeParts | DatetimeParts[] to DatetimeParts, which year field has type number. It cannot be undefined! So in the else block data is typed as never.

P.S. DatetimeParts is defined as:

interface DatetimeParts {
  month: number;
  day: number | null;
  year: number;
  dayOfWeek?: number | null;
  hour?: number;
  minute?: number;
  ampm?: 'am' | 'pm';
}

Maybe they forgot to set year?: number | number? Dunno.

@jfet97
Copy link
Contributor Author

jfet97 commented Dec 17, 2024

microsoft/vscode

In addition to this.

We have something interesting when it comes to error TS7022: 'groupEntry' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer. at src/vs/editor/standalone/common/monarch/monarchLexer.ts#L614.

This is interesting because TS loses the ability to solve a circular type dependency while inferring. There is this situation where types of identifiers are mutually dependent. We are in a big while loop with multiple re-assignments: groupEntry and matches depend on groupMatching, which starts as null but gets reassigned near the end, and from groupEntry we get action which will be part, directly or indirectly, of the next version of groupMatching itself:

let groupMatching: GroupMatching | null = null;

let forceEvaluation = true;
let pos = 0
let lineLength = 0

while (forceEvaluation || pos < lineLength) {
  let matches: string[] | null = null;
  let action: FuzzyAction | FuzzyAction[] | null = null;
  let rule: IRule | null = null;

  if (groupMatching) {
    const groupEntry = groupMatching.groups.shift()!; // <-- new error here

    // they depend on groupMatching
    action = groupEntry.action;
    matches = groupMatching.matches;
    rule = groupMatching.rule;
  }

  // things happens with action and matches
  result = ...

  // recursive dependency here
  if (matches.length === fn(result)) {
    // and here
    groupMatching = {
      rule: rule,
      matches: matches,
      groups: []
    };
  }
}

I don't know the reason why the new discriminating capability doesn't let TS solve this circularity anymore. Of course we could easily fix this situation by manually solving the circularity:

const groupEntry: {
  action: monarchCommon.FuzzyAction;
  matched: string;
} = groupMatching.groups.shift()!;

As a bonus, the problem seems to be related to the "recursive" inferred type for groupMatching. In fact, even the following type assertion solves this issue:

groupMatching = {
  rule: rule,
  matches: matches,
  groups: []
} as GroupMatching;

 

src/vs/platform/workspaces/node/workspaces.ts#L76:

if (typeof folderStat.birthtimeMs === 'number') {
  ctime = Math.floor(folderStat.birthtimeMs); 
} else {
  ctime = folderStat.birthtime.getTime(); // <-- new error here
}

We have that folderStat was already narrowed before from Stats | undefined to Stats. But birthtimeMs has type number, undefined is not allowed! So folderStat becomes never in that else branch.

src/vs/workbench/services/label/common/labelService.ts#L278:

if (hasMultipleRoots && !options.noPrefix) {
  const rootName = folder?.name ?? basenameOrAuthority(folder.uri); // <-- new error here
  relativeLabel = relativeLabel ? `${rootName}${relativeLabel}` : rootName;
}

Same story: folder was narrowed and its name property has type string so it cannot be undefined. Probably they should use || to catch the empty string case if their intention was to provide a fallback.

src/vs/workbench/services/workingCopy/common/workingCopyBackupService.ts#L515

if (typeof meta?.typeId === 'string') {
    delete meta.typeId; // <-- new error here
    
    if (isEmptyObject(meta)) {
    meta = undefined;
    }
}

The type of typeId is ?: undefined, so that typeof check is now considered by TS as impossible to be satisfied.

@jfet97
Copy link
Contributor Author

jfet97 commented Dec 17, 2024

Redocly/redoc

We have:

const { type, oneOf, discriminatorProp, isCircular } = schema;
// discriminatorProp has type string

// ...

if(discriminatorProp !== undefined) {
  // ...
}

// schema is never from now on, so new errors here:
schema.fields?.length
schema.description
schema.externalDocs

Here discriminatorProp, that comes from destructuring schema itself, has type string. It cannot be undefined! Therefore it is typed as never after that if, and so will be schema.

@jfet97
Copy link
Contributor Author

jfet97 commented Dec 17, 2024

@gabritto hey there!

I went through all the errors, and apart from some occurrences of never here and there due to better narrowing or a constituent of a union disappearing, because of properties defined with the type never (#60702), there was just one main relevant error.

It is the first one from microsoft/vscode that I've discussed here. TS isn't able to solve a messy circular dependency while inferring anymore, so it needs a little help now. Not sure if that's a big deal actually: the fix is easy, this type of error occurs only once among those reported and I'd say it would be quite uncommon in user-land given how you should structure your code to encounter it. Having said that, it remains relevant because it might be highlighting some hidden issue in the resolution of circularities, which I would venture to assume falls outside the scope of this PR.

In the previous messages you'll find notes about this and the other new errors, with references to older messages when needed. If there is anything else I could do to help, let me know!

@jakebailey
Copy link
Member

In #60718 (comment), I don't think it's right to say that the user "doesn't need assertNever"; it seems to me that assertNever is in cases specifically because it's in dead code...

@jfet97
Copy link
Contributor Author

jfet97 commented Dec 17, 2024

In #60718 (comment), I don't think it's right to say that the user "doesn't need assertNever"; it seems to me that assertNever is in cases specifically because it's in dead code...

Yeah, it was just a stupid joke because sometimes it seems to me that this PR is all about never. I'd say that code should be changed in:

assertNever(currentDragElement, `Unknown drag element type: ${currentDragElement}`)

// and

assertNever(selectedFlow, `Unknown flow type: ${selectedFlow}`)

removing the .type accesses.

@jfet97
Copy link
Contributor Author

jfet97 commented Dec 18, 2024

@jakebailey may I ask for another "pack this"? It would be easier to play with it. The previous one is outdated given the implementation changes. Thanks ❤️

@jakebailey
Copy link
Member

@typescript-bot pack this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Dec 18, 2024

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

Command Status Results
pack this ✅ Started ✅ Results

@typescript-bot
Copy link
Collaborator

typescript-bot commented Dec 18, 2024

Hey @jakebailey, I've packed this into an installable tgz. You can install it for testing by referencing it in your package.json like so:

{
    "devDependencies": {
        "typescript": "https://typescript.visualstudio.com/cf7ac146-d525-443c-b23c-0d58337efebc/_apis/build/builds/164424/artifacts?artifactName=tgz&fileId=9CD0E7EEA3917E1438A7400443199DC067F8AC9C8ACD7163731419F14DA6367202&fileName=/typescript-5.8.0-insiders.20241218.tgz"
    }
}

and then running npm install.


There is also a playground for this build and an npm module you can use via "typescript": "npm:@typescript-deploys/pr-build@5.8.0-pr-60718-47".;

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.

6 participants