Skip to content

Conversation

@Andarist
Copy link
Contributor

it's an extension of the algorithm introduced in #48538
closes #53018
fixes #48798

@typescript-bot typescript-bot added the For Backlog Bug PRs that fix a backlog bug label Apr 26, 2023
Comment on lines 24114 to 24115
const sourceValueDeclaration = sourceType.symbol?.valueDeclaration;
if (sourceValueDeclaration) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I expect that this part is not fully correct. What I'd like to do here is to only get sourceValueDeclaration if it's an anonymous/fresh/smth declaration within within the argument position. I'm not sure how to check for this.

Copy link
Member

Choose a reason for hiding this comment

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

Use findAncestor to check for argument position membership, use ObjectFlags.FreshLiteral to check for freshness on the type.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added .valueDeclaration to reverse mapped properties. This grants me clean access to the node I'm looking for and fixes an issue with array literals used as values of object reverse mapped types (test case). I think that this gives me now exactly what I'm looking for when combined with the freshness check and I don't need to do any findAncestor traversal.

inferTypes([inference], sourceType, templateType);
const sourceValueDeclaration = sourceType.symbol?.valueDeclaration;
if (sourceValueDeclaration) {
const intraExpressionInferenceSites = getInferenceContext(sourceValueDeclaration)?.intraExpressionInferenceSites?.filter(site => isNodeDescendantOf(site.node, sourceValueDeclaration));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The .filter here is unfortunate but my current goal is to make it correct and only after that I can try to make it fast 😉

The goal here is to only try to infer from the intra expression inference sites relevant for this specific reverse mapped symbol. Perhaps the simplest and good enough solution would be to get an appropriate trailing slice of all intraExpressionInferenceSites.

They are "aggregated" throughout the call to checkExpressionWithContextualType so when encountering a first context sensitive function it's a single-element array and when encountering a second one it's a two-element array and so on. inferFromIntraExpressionSites happens while pushing items into intraExpressionInferenceSites so that's why the producer has to always come first before the consumer (TS playground):

declare function f<T>(arg: {
  produce: (n: string) => T;
  consume: (x: T) => void;
}): void;

f({
  produce: (n) => n,
  consume: (x) => x.toLowerCase(), // ok, `x` is inferred
});

f({
  consume: (x) => x.toLowerCase(), // doesn't work, 'x' is of type 'unknown'.(18046)
  produce: (n) => n,
});

And thus, by extension... if we'd look for "relevant" nodes from the end we could slice the trailing elements until we meet one that is not relevant. Because intra intra expression inference sites are dependent on source order this should work just fine as it should be guaranteed that by using such a trailing slice we use all current "relevant" nodes and nothing else

Copy link
Member

Choose a reason for hiding this comment

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

But doesn't this all just imply we should store intra-expression inference sites in a per-property map or something?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

do u mean properties of the reverse mapped type? IIRC we might not know if a property is going to end up as property of such - or I don't know how to check that. So I don't know how to aggregate such a structure since I don't know when it should be created in the first place.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since I opened this PR I got a way better understanding of how the whole intra-expression inference works. What is in this PR right now isn't ideal on the high level of things since it comes with a perf penalty cost. I'm trying to figure out how to best store those sites to accommodate the needs of this change and I'm hitting the wall.

Some things to understand about this inference type:

  1. those sites are continuously ~ gathered in objects/arrays and they are cleared when something "pulls" the information from them
  2. this is OK because the inference actually infers into all type parameters and not only into the one that got "pulled"
  3. given the recursive nature of the algorithm sites are gathered in the reverse order - ancestor nodes might only be gathered after descendants because we need to exit the inner work to get the type of an ancestor
  4. there is quite some repetition in the inference here because if we end up inferring from the ancestor that will also include inferring from its descendants and those descendants might also be in the gathered sites. I was experimenting with some changes to the algorithm to "replace" inner sites with outer ones to skip over some redundant work here. That's only a partial solution though because we can always end up with a sequence like this: push a.b.c > replace with a.b > pull > push a. In a situation like this, we might still infer from the whole a even though we already inferred from a.b and there is no mechanism to "skip over" this slice of the object. So I didn't end up pursuing this optimization as it seems that it's only a partial one.
  5. In the most common situations though we usually just enter a different part of the node after pulling from those sites so the redundancy problem is likely not that big. Often we just push something, pull from it, clear the sites, push again info unrelated to what we already processed and the cycle continues. So what this PR does right now is that it doesn't clear those sites. This isn't incorrect - it's just redundant since all sites are left behind and each pull infers from all the ones that were gathered up so far.
  6. Reverse mapped type inference happens at different stages of the overall algorithm, it's a little bit "tacked on" the standard inference thing - it doesn't exactly play by the same rules etc. A special type/symbol is set up as an inference candidate for it and when its members are accessed then we infer from those. This is nice because the whole thing doesn't have to be processed immediately, it happens "on-demand"
  7. So what happens today on main is that those intra-expression inference sites are cleared before the reverse mapped type has a chance to infer from it.
  8. What we can deduce from all of that is that we still need to clear up those sites for the "regular inference" but somehow "keep them around" for the reverse mapped type inference.
  9. Ideally, we should also have a way of "scoping" those sites within any given object/array property so the reverse mapped type inference could only try to infer from the sites relevant to that slice of an object instead of trying to infer from all of the ones that we were able to gather.

So I'm looking for some way to store/read/clear those sites without hitting any of the mentioned drawbacks and for two different/similar-ush purposes but I can't figure this out so far. cc @jakebailey

Comment on lines 24121 to 24123
pushContextualType(sourceValueDeclaration as any as Expression, templateType, /*isCache*/ false);
inferFromIntraExpressionSites([inference], intraExpressionInferenceSites);
popContextualType();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

At first this felt quite hacky but now I'm leaning towards saying that it's the easiest and cleanest solution to this problem. The problem that this is solving (together with the getApparentTypeOfContextualType(sourceValueDeclaration.parent.parent) call a few lines back is that it avoids instantiating the property name. Essentially it allows me use the bare templateType without substituting indexed mapped types so inferFromIntraExpressionSites can see a target type like (x: string) => T[K] instead of (x: string) => T["a"] and thus it can infer things for T[K].

I wasn't sure what value I should pass as isCache here though. I'm pretty sure that @ahejlsberg would know this and could advise here

Comment on lines 1 to 4
tests/cases/compiler/mappedTypeContextualTypesApplied.ts(18,10): error TS7023: 'foo' implicitly has return type 'any' because it does not have a return type annotation and is referenced directly or indirectly in one of its return expressions.
tests/cases/compiler/mappedTypeContextualTypesApplied.ts(18,15): error TS7006: Parameter 's' implicitly has an 'any' type.
tests/cases/compiler/mappedTypeContextualTypesApplied.ts(19,10): error TS7023: 'foo' implicitly has return type 'any' because it does not have a return type annotation and is referenced directly or indirectly in one of its return expressions.
tests/cases/compiler/mappedTypeContextualTypesApplied.ts(19,15): error TS7006: Parameter 's' implicitly has an 'any' type.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Those are not expected. I'm almost certain that all of those would get fixed if this PR would get reviewed and merged: #52095 . This currently fails exactly because the contextual type involves an intersection and getTypeOfPropertyOfContextualType doesn't handle such cases appropriately.

Comment on lines 1 to 6
tests/cases/conformance/types/typeRelationships/typeInference/intraExpressionInferencesReverseMappedTypes.ts(67,21): error TS18046: 'x' is of type 'unknown'.
tests/cases/conformance/types/typeRelationships/typeInference/intraExpressionInferencesReverseMappedTypes.ts(71,21): error TS18046: 'x' is of type 'unknown'.
tests/cases/conformance/types/typeRelationships/typeInference/intraExpressionInferencesReverseMappedTypes.ts(80,21): error TS18046: 'x' is of type 'unknown'.
tests/cases/conformance/types/typeRelationships/typeInference/intraExpressionInferencesReverseMappedTypes.ts(86,21): error TS18046: 'x' is of type 'unknown'.
tests/cases/conformance/types/typeRelationships/typeInference/intraExpressionInferencesReverseMappedTypes.ts(95,21): error TS18046: 'x' is of type 'unknown'.
tests/cases/conformance/types/typeRelationships/typeInference/intraExpressionInferencesReverseMappedTypes.ts(101,21): error TS18046: 'x' is of type 'unknown'.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

All of those relate to the tuple-based tests that I added and those are not expected.

It doesn't work because in tuples both T and T[K] are not reverse mapped symbols/types and thus the inferReverseMappedType isn't called from within checkExpressionWithContextualType (and only within that call intraExpressionInferenceSites and "pushed inference contexts" are available).

With objects createReverseMappedType only creates a ReverseMapped type and doesn't do much else. inferReverseMappedType gets called for properties later from getTypeOfReverseMappedSymbol). However, with tuples it calls inferReverseMappedType eagerly:
https://github.dev/microsoft/TypeScript/blob/eb014a26522dd809ae4d0e85634a62eabda2755a/src/compiler/checker.ts#L24090-L24101

I could try to fix this in this PR but I also feel like this would require changes that could be moved to a separate followup PR. This PR could focus on the base mechanism for this improvement and on object-based cases.

export function getDeclarationModifierFlagsFromSymbol(s: Symbol, isWrite = false): ModifierFlags {
if (s.valueDeclaration) {
const checkFlags = getCheckFlags(s);
if (!(checkFlags & CheckFlags.ReverseMapped) && s.valueDeclaration) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since I added .valueDeclaration to reverse mapped properties, we need to ignore this here - otherwise, we break stripping of readonly modifiers that is required by #12589

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Basically, this change just maintains compatibility with the old behavior as 0 was always returned from here in the implicit case of checkFlags & CheckFlags.ReverseMapped

@Andarist Andarist marked this pull request as ready for review November 29, 2025 11:37
@jakebailey
Copy link
Member

@typescript-bot test it
@typescript-bot pack this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Nov 29, 2025

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

Command Status Results
pack this ✅ Started ✅ 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

typescript-bot commented Nov 29, 2025

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/166623/artifacts?artifactName=tgz&fileId=D5535CEF730027ECCAE923707D8DBA2C2406C90B822EBB402EE97C99CB6F116402&fileName=/typescript-6.0.0-insiders.20251129.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/[email protected]".;

@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 1 1 ~ ~ ~ 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 195,162k (± 0.93%) 195,598k (± 0.81%) ~ 192,390k 196,419k p=0.810 n=6
Parse Time 1.59s (± 1.57%) 1.61s (± 1.45%) ~ 1.58s 1.64s p=0.686 n=6
Bind Time 0.91s (± 0.92%) 0.91s (± 1.70%) ~ 0.89s 0.93s p=0.667 n=6
Check Time 12.00s (± 0.64%) 11.98s (± 0.65%) ~ 11.85s 12.08s p=0.573 n=6
Emit Time 3.44s (± 4.25%) 3.50s (± 3.28%) ~ 3.34s 3.60s p=0.377 n=6
Total Time 17.94s (± 0.59%) 17.99s (± 0.98%) ~ 17.66s 18.17s p=0.378 n=6
angular-1 - node (v18.15.0, x64)
Errors 2 2 ~ ~ ~ p=1.000 n=6
Symbols 956,047 956,050 +3 (+ 0.00%) ~ ~ p=0.001 n=6
Types 415,881 415,884 +3 (+ 0.00%) ~ ~ p=0.001 n=6
Memory used 1,255,061k (± 0.01%) 1,255,019k (± 0.00%) ~ 1,254,933k 1,255,090k p=0.378 n=6
Parse Time 6.55s (± 0.63%) 6.54s (± 0.83%) ~ 6.48s 6.60s p=1.000 n=6
Bind Time 1.96s (± 0.26%) 1.97s (± 0.26%) ~ 1.96s 1.97s p=0.311 n=6
Check Time 32.42s (± 0.52%) 32.37s (± 0.32%) ~ 32.24s 32.53s p=0.423 n=6
Emit Time 14.89s (± 0.40%) 14.90s (± 0.36%) ~ 14.85s 14.97s p=0.748 n=6
Total Time 55.82s (± 0.34%) 55.77s (± 0.29%) ~ 55.61s 55.99s p=0.688 n=6
mui-docs - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 2,724,073 2,724,073 ~ ~ ~ p=1.000 n=6
Types 937,413 937,413 ~ ~ ~ p=1.000 n=6
Memory used 3,053,157k (± 0.00%) 3,053,174k (± 0.00%) ~ 3,053,006k 3,053,275k p=0.575 n=6
Parse Time 8.58s (± 0.19%) 8.60s (± 0.15%) ~ 8.58s 8.62s p=0.188 n=6
Bind Time 2.33s (± 0.58%) 2.33s (± 0.35%) ~ 2.32s 2.34s p=0.604 n=6
Check Time 93.04s (± 0.43%) 93.37s (± 0.51%) ~ 93.05s 94.31s p=0.128 n=6
Emit Time 0.31s (± 1.68%) 0.31s (± 1.68%) ~ 0.30s 0.31s p=1.000 n=6
Total Time 104.26s (± 0.38%) 104.60s (± 0.45%) ~ 104.30s 105.55s p=0.199 n=6
self-build-src - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 1,251,545 1,251,621 +76 (+ 0.01%) ~ ~ p=0.001 n=6
Types 259,756 259,795 +39 (+ 0.02%) ~ ~ p=0.001 n=6
Memory used 3,003,450k (± 9.90%) 3,003,455k (± 9.88%) ~ 2,397,253k 3,125,558k p=0.873 n=6
Parse Time 6.57s (± 0.68%) 6.59s (± 1.12%) ~ 6.47s 6.68s p=0.230 n=6
Bind Time 2.20s (± 1.30%) 2.20s (± 1.42%) ~ 2.18s 2.26s p=0.518 n=6
Check Time 42.87s (± 0.28%) 42.96s (± 0.49%) ~ 42.71s 43.25s p=0.575 n=6
Emit Time 3.44s (± 3.66%) 3.47s (± 2.61%) ~ 3.34s 3.62s p=0.470 n=6
Total Time 55.08s (± 0.39%) 55.23s (± 0.44%) ~ 54.95s 55.60s p=0.378 n=6
self-build-src-public-api - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 1,251,545 1,251,621 +76 (+ 0.01%) ~ ~ p=0.001 n=6
Types 259,756 259,795 +39 (+ 0.02%) ~ ~ p=0.001 n=6
Memory used 2,459,270k (± 0.01%) 2,459,858k (± 0.02%) +588k (+ 0.02%) 2,459,368k 2,460,443k p=0.020 n=6
Parse Time 5.32s (± 1.41%) 5.34s (± 0.72%) ~ 5.31s 5.40s p=0.748 n=6
Bind Time 1.88s (± 1.45%) 1.88s (± 0.45%) ~ 1.87s 1.89s p=0.560 n=6
Check Time 35.59s (± 0.35%) 35.57s (± 0.26%) ~ 35.44s 35.69s p=0.936 n=6
Emit Time 3.02s (± 1.40%) 3.06s (± 1.91%) ~ 2.99s 3.14s p=0.230 n=6
Total Time 45.82s (± 0.36%) 45.85s (± 0.11%) ~ 45.81s 45.94s p=0.471 n=6
self-compiler - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 264,177 264,231 +54 (+ 0.02%) ~ ~ p=0.001 n=6
Types 103,979 104,018 +39 (+ 0.04%) ~ ~ p=0.001 n=6
Memory used 443,114k (± 0.01%) 443,188k (± 0.01%) +74k (+ 0.02%) 443,142k 443,231k p=0.008 n=6
Parse Time 2.84s (± 0.48%) 2.84s (± 0.73%) ~ 2.82s 2.87s p=0.508 n=6
Bind Time 1.15s (± 0.35%) 1.16s (± 0.71%) ~ 1.15s 1.17s p=0.248 n=6
Check Time 16.04s (± 0.37%) 16.03s (± 0.23%) ~ 15.97s 16.06s p=0.806 n=6
Emit Time 1.32s (± 1.39%) 1.33s (± 1.16%) ~ 1.32s 1.36s p=0.459 n=6
Total Time 21.36s (± 0.32%) 21.36s (± 0.25%) ~ 21.29s 21.42s p=0.872 n=6
ts-pre-modules - node (v18.15.0, x64)
Errors 72 72 ~ ~ ~ p=1.000 n=6
Symbols 225,386 225,386 ~ ~ ~ p=1.000 n=6
Types 94,304 94,304 ~ ~ ~ p=1.000 n=6
Memory used 370,129k (± 0.03%) 370,087k (± 0.04%) ~ 369,903k 370,314k p=0.471 n=6
Parse Time 2.82s (± 0.73%) 2.86s (± 0.78%) +0.04s (+ 1.30%) 2.83s 2.89s p=0.029 n=6
Bind Time 1.66s (± 0.83%) 1.64s (± 1.05%) ~ 1.62s 1.66s p=0.144 n=6
Check Time 16.61s (± 0.28%) 16.54s (± 0.38%) ~ 16.48s 16.62s p=0.089 n=6
Emit Time 0.00s (±244.70%) 0.00s ~ ~ ~ p=0.405 n=6
Total Time 21.09s (± 0.31%) 21.04s (± 0.22%) ~ 20.97s 21.10s p=0.148 n=6
vscode - node (v18.15.0, x64)
Errors 11 11 ~ ~ ~ p=1.000 n=6
Symbols 4,068,329 4,068,331 +2 (+ 0.00%) ~ ~ p=0.001 n=6
Types 1,281,534 1,281,577 +43 (+ 0.00%) ~ ~ p=0.001 n=6
Memory used 3,854,254k (± 0.00%) 3,854,241k (± 0.00%) ~ 3,854,161k 3,854,313k p=0.748 n=6
Parse Time 15.66s (± 0.41%) 15.71s (± 0.59%) ~ 15.62s 15.89s p=0.334 n=6
Bind Time 5.29s (± 0.19%) 5.31s (± 2.51%) ~ 5.23s 5.58s p=0.326 n=6
Check Time 112.41s (± 2.10%) 112.44s (± 1.82%) ~ 109.07s 114.82s p=1.000 n=6
Emit Time 41.77s (±12.02%) 39.01s (± 0.70%) ~ 38.77s 39.54s p=0.298 n=6
Total Time 175.13s (± 3.46%) 172.48s (± 1.16%) ~ 169.50s 175.24s p=0.471 n=6
webpack - node (v18.15.0, x64)
Errors 40 40 ~ ~ ~ p=1.000 n=6
Symbols 379,892 379,892 ~ ~ ~ p=1.000 n=6
Types 166,631 166,631 ~ ~ ~ p=1.000 n=6
Memory used 540,036k (± 0.02%) 540,070k (± 0.01%) ~ 539,977k 540,139k p=0.575 n=6
Parse Time 4.70s (± 0.90%) 4.70s (± 1.01%) ~ 4.61s 4.75s p=0.936 n=6
Bind Time 2.03s (± 1.41%) 2.05s (± 1.10%) ~ 2.02s 2.08s p=0.629 n=6
Check Time 22.90s (± 0.27%) 22.93s (± 0.41%) ~ 22.81s 23.07s p=0.629 n=6
Emit Time 0.00s 0.00s ~ ~ ~ p=1.000 n=6
Total Time 29.63s (± 0.19%) 29.66s (± 0.28%) ~ 29.57s 29.81s p=0.572 n=6
xstate-main - node (v18.15.0, x64)
Errors 30 36 🔻+6 (+20.00%) ~ ~ p=0.001 n=6
Symbols 690,397 690,751 +354 (+ 0.05%) ~ ~ p=0.001 n=6
Types 208,790 208,845 +55 (+ 0.03%) ~ ~ p=0.001 n=6
Memory used 586,176k (± 0.03%) 586,661k (± 0.02%) +485k (+ 0.08%) 586,492k 586,762k p=0.005 n=6
Parse Time 4.17s (± 0.66%) 4.16s (± 0.70%) ~ 4.13s 4.20s p=0.517 n=6
Bind Time 1.40s (± 1.48%) 1.40s (± 1.60%) ~ 1.38s 1.44s p=0.806 n=6
Check Time 20.54s (± 1.66%) 20.54s (± 1.60%) ~ 20.26s 20.97s p=1.000 n=6
Emit Time 0.01s (±109.43%) 0.00s (±244.70%) ~ 0.00s 0.01s p=0.282 n=6
Total Time 26.11s (± 1.25%) 26.11s (± 1.34%) ~ 25.80s 26.60s p=0.810 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 user tests with tsc comparing main and refs/pull/54029/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 Here are the results of running the top 400 repos with tsc comparing main and refs/pull/54029/merge:

Everything looks good!

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

Labels

For Backlog Bug PRs that fix a backlog bug

Projects

Status: Waiting on reviewers

5 participants