Skip to content

Default reverse mapped type inference to its constraint #56300

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

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

Conversation

Andarist
Copy link
Contributor

@Andarist Andarist commented Nov 3, 2023

closes #56241

@typescript-bot typescript-bot added the For Backlog Bug PRs that fix a backlog bug label Nov 3, 2023
@jakebailey
Copy link
Member

@typescript-bot test top200
@typescript-bot user test this
@typescript-bot run dt
@typescript-bot perf test this
@typescript-bot pack this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Nov 3, 2023

Heya @jakebailey, I've started to run the parallelized Definitely Typed test suite on this PR at 720a860. You can monitor the build here.

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Nov 3, 2023

Heya @jakebailey, I've started to run the tarball bundle task on this PR at 720a860. You can monitor the build here.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Nov 3, 2023

Heya @jakebailey, I've started to run the diff-based user code test suite on this PR at 720a860. You can monitor the build here.

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Nov 3, 2023

Heya @jakebailey, I've started to run the regular perf test suite on this PR at 720a860. You can monitor the build here.

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Nov 3, 2023

Heya @jakebailey, I've started to run the diff-based top-repos suite on this PR at 720a860. You can monitor the build here.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Nov 3, 2023

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/158454/artifacts?artifactName=tgz&fileId=69B8D4D93203CCB3C86748B20CB1D44417DE59215D6FAF4B9363F872E287B43D02&fileName=/typescript-5.4.0-insiders.20231103.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

@jakebailey Here are the results of running the user test suite comparing main and refs/pull/56300/merge:

There were infrastructure failures potentially unrelated to your change:

  • 3 instances of "Package install failed"

Otherwise...

Everything looks good!

@typescript-bot
Copy link
Collaborator

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

Here they are:

Compiler

Comparison Report - baseline..pr
Metric baseline pr Delta Best Worst p-value
Angular - node (v18.15.0, x64)
Memory used 295,139k (± 0.01%) 295,142k (± 0.01%) ~ 295,068k 295,182k p=0.810 n=6
Parse Time 2.63s (± 0.54%) 2.63s (± 0.44%) ~ 2.62s 2.65s p=0.934 n=6
Bind Time 0.84s (± 0.97%) 0.83s (± 1.00%) ~ 0.83s 0.85s p=0.718 n=6
Check Time 8.04s (± 0.22%) 8.04s (± 0.24%) ~ 8.02s 8.07s p=1.000 n=6
Emit Time 7.08s (± 0.30%) 7.09s (± 0.29%) ~ 7.06s 7.11s p=0.628 n=6
Total Time 18.58s (± 0.14%) 18.60s (± 0.17%) ~ 18.56s 18.64s p=0.747 n=6
Compiler-Unions - node (v18.15.0, x64)
Memory used 192,642k (± 1.57%) 193,067k (± 1.49%) ~ 190,643k 196,550k p=1.000 n=6
Parse Time 1.35s (± 1.02%) 1.35s (± 0.00%) ~ 1.35s 1.35s p=0.599 n=6
Bind Time 0.73s (± 0.00%) 0.73s (± 0.00%) ~ 0.73s 0.73s p=1.000 n=6
Check Time 9.17s (± 0.32%) 9.15s (± 0.29%) ~ 9.12s 9.19s p=0.225 n=6
Emit Time 2.63s (± 0.44%) 2.63s (± 0.56%) ~ 2.61s 2.65s p=0.680 n=6
Total Time 13.88s (± 0.26%) 13.87s (± 0.25%) ~ 13.84s 13.93s p=0.519 n=6
Monaco - node (v18.15.0, x64)
Memory used 347,352k (± 0.00%) 347,348k (± 0.00%) ~ 347,327k 347,364k p=0.748 n=6
Parse Time 2.46s (± 0.56%) 2.47s (± 0.22%) ~ 2.46s 2.47s p=0.138 n=6
Bind Time 0.94s (± 0.00%) 0.94s (± 0.43%) ~ 0.94s 0.95s p=0.405 n=6
Check Time 6.91s (± 0.53%) 6.93s (± 0.30%) ~ 6.90s 6.95s p=0.511 n=6
Emit Time 4.04s (± 0.49%) 4.05s (± 0.44%) ~ 4.03s 4.08s p=0.677 n=6
Total Time 14.35s (± 0.15%) 14.38s (± 0.17%) ~ 14.35s 14.41s p=0.085 n=6
TFS - node (v18.15.0, x64)
Memory used 302,581k (± 0.01%) 302,600k (± 0.01%) ~ 302,573k 302,647k p=0.297 n=6
Parse Time 2.01s (± 0.87%) 2.00s (± 1.16%) ~ 1.98s 2.04s p=0.739 n=6
Bind Time 1.01s (± 1.02%) 1.01s (± 1.15%) ~ 1.00s 1.03s p=0.801 n=6
Check Time 6.25s (± 0.48%) 6.26s (± 0.36%) ~ 6.24s 6.30s p=0.421 n=6
Emit Time 3.58s (± 0.46%) 3.57s (± 0.34%) ~ 3.56s 3.59s p=0.191 n=6
Total Time 12.85s (± 0.25%) 12.84s (± 0.23%) ~ 12.81s 12.89s p=0.686 n=6
material-ui - node (v18.15.0, x64)
Memory used 470,512k (± 0.01%) 470,516k (± 0.01%) ~ 470,480k 470,571k p=1.000 n=6
Parse Time 2.58s (± 0.32%) 2.57s (± 0.29%) ~ 2.56s 2.58s p=0.383 n=6
Bind Time 0.99s (± 0.64%) 0.99s (± 1.28%) ~ 0.97s 1.00s p=0.799 n=6
Check Time 16.62s (± 0.37%) 16.65s (± 0.60%) ~ 16.54s 16.79s p=0.936 n=6
Emit Time 0.00s (± 0.00%) 0.00s (± 0.00%) ~ 0.00s 0.00s p=1.000 n=6
Total Time 20.18s (± 0.31%) 20.21s (± 0.51%) ~ 20.11s 20.36s p=0.747 n=6
xstate - node (v18.15.0, x64)
Memory used 512,648k (± 0.01%) 512,691k (± 0.01%) ~ 512,619k 512,793k p=0.173 n=6
Parse Time 3.27s (± 0.36%) 3.28s (± 0.23%) ~ 3.27s 3.29s p=0.279 n=6
Bind Time 1.55s (± 0.33%) 1.54s (± 0.33%) ~ 1.54s 1.55s p=0.311 n=6
Check Time 2.84s (± 0.74%) 2.84s (± 0.36%) ~ 2.82s 2.85s p=0.743 n=6
Emit Time 0.08s (± 0.00%) 0.08s (± 4.99%) ~ 0.08s 0.09s p=0.405 n=6
Total Time 7.73s (± 0.21%) 7.74s (± 0.12%) ~ 7.73s 7.75s p=0.365 n=6
System info unknown
Hosts
  • node (v18.15.0, x64)
Scenarios
  • Angular - node (v18.15.0, x64)
  • Compiler-Unions - node (v18.15.0, x64)
  • Monaco - node (v18.15.0, x64)
  • TFS - node (v18.15.0, x64)
  • material-ui - node (v18.15.0, x64)
  • xstate - node (v18.15.0, x64)
Benchmark Name Iterations
Current pr 6
Baseline baseline 6

tsserver

Comparison Report - baseline..pr
Metric baseline pr Delta Best Worst p-value
Compiler-UnionsTSServer - node (v18.15.0, x64)
Req 1 - updateOpen 2,391ms (± 1.47%) 2,380ms (± 1.11%) ~ 2,359ms 2,428ms p=0.872 n=6
Req 2 - geterr 5,350ms (± 1.63%) 5,386ms (± 1.70%) ~ 5,279ms 5,486ms p=0.378 n=6
Req 3 - references 328ms (± 1.01%) 326ms (± 1.04%) ~ 323ms 332ms p=0.462 n=6
Req 4 - navto 279ms (± 1.46%) 277ms (± 1.26%) ~ 273ms 280ms p=0.284 n=6
Req 5 - completionInfo count 1,356 (± 0.00%) 1,356 (± 0.00%) ~ 1,356 1,356 p=1.000 n=6
Req 5 - completionInfo 81ms (± 9.14%) 85ms (± 7.45%) ~ 75ms 90ms p=0.402 n=6
CompilerTSServer - node (v18.15.0, x64)
Req 1 - updateOpen 2,492ms (± 0.57%) 2,503ms (± 1.22%) ~ 2,451ms 2,531ms p=0.298 n=6
Req 2 - geterr 4,056ms (± 1.53%) 4,134ms (± 1.73%) +77ms (+ 1.91%) 4,039ms 4,195ms p=0.031 n=6
Req 3 - references 342ms (± 1.34%) 336ms (± 1.44%) ~ 332ms 343ms p=0.076 n=6
Req 4 - navto 283ms (± 0.30%) 281ms (± 0.29%) -1ms (- 0.41%) 280ms 282ms p=0.045 n=6
Req 5 - completionInfo count 1,518 (± 0.00%) 1,518 (± 0.00%) ~ 1,518 1,518 p=1.000 n=6
Req 5 - completionInfo 87ms (± 5.15%) 80ms (± 5.97%) 🟩-7ms (- 7.84%) 77ms 89ms p=0.038 n=6
xstateTSServer - node (v18.15.0, x64)
Req 1 - updateOpen 2,594ms (± 0.25%) 2,589ms (± 0.68%) ~ 2,562ms 2,611ms p=0.687 n=6
Req 2 - geterr 1,692ms (± 1.69%) 1,727ms (± 2.55%) ~ 1,668ms 1,779ms p=0.229 n=6
Req 3 - references 118ms (± 8.12%) 111ms (± 7.49%) ~ 105ms 125ms p=0.170 n=6
Req 4 - navto 364ms (± 0.25%) 365ms (± 0.30%) ~ 364ms 367ms p=0.149 n=6
Req 5 - completionInfo count 2,073 (± 0.00%) 2,073 (± 0.00%) ~ 2,073 2,073 p=1.000 n=6
Req 5 - completionInfo 309ms (± 1.14%) 310ms (± 1.80%) ~ 300ms 316ms p=0.422 n=6
System info unknown
Hosts
  • node (v18.15.0, x64)
Scenarios
  • CompilerTSServer - node (v18.15.0, x64)
  • Compiler-UnionsTSServer - node (v18.15.0, x64)
  • xstateTSServer - node (v18.15.0, x64)
Benchmark Name Iterations
Current pr 6
Baseline baseline 6

Startup

Comparison Report - baseline..pr
Metric baseline pr Delta Best Worst p-value
tsc-startup - node (v18.15.0, x64)
Execution time 152.20ms (± 0.17%) 152.17ms (± 0.19%) ~ 150.92ms 156.05ms p=0.193 n=600
tsserver-startup - node (v18.15.0, x64)
Execution time 227.91ms (± 0.18%) 227.66ms (± 0.16%) -0.25ms (- 0.11%) 226.18ms 232.66ms p=0.000 n=600
tsserverlibrary-startup - node (v18.15.0, x64)
Execution time 228.96ms (± 0.17%) 228.90ms (± 0.17%) ~ 227.31ms 236.30ms p=0.096 n=600
typescript-startup - node (v18.15.0, x64)
Execution time 228.91ms (± 0.16%) 228.94ms (± 0.15%) ~ 227.59ms 231.34ms p=0.188 n=600
System info unknown
Hosts
  • node (v18.15.0, x64)
Scenarios
  • tsc-startup - node (v18.15.0, x64)
  • tsserver-startup - node (v18.15.0, x64)
  • tsserverlibrary-startup - node (v18.15.0, x64)
  • typescript-startup - node (v18.15.0, x64)
Benchmark Name Iterations
Current pr 6
Baseline baseline 6

Developer Information:

Download Benchmarks

@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.

@@ -24853,7 +24853,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const templateType = getTemplateTypeFromMappedType(target);
const inference = createInferenceInfo(typeParameter);
inferTypes([inference], sourceType, templateType);
return getTypeFromInference(inference) || unknownType;
return getTypeFromInference(inference) || getBaseConstraintOfType(typeParameter) || unknownType;
Copy link
Member

Choose a reason for hiding this comment

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

getBaseConstraintOfType recursively walks all constraints - so if T extends U and U extends V and V extends number, you get number out. Which means you skip inferences to those intermediate results. I think you'd want to use getConstraintOfType, right? Because, in this case, if you have T[K] extends U and U extends number, U is a better (and more specific) answer than number.

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 think you'd want to use getConstraintOfType, right?

Sort of. It's true it won't immediately walk all constraints but at the same time, it's not inference-aware anyway. If I call it then I end up with T[keyof T] and when relating the inferred type to the instantiated constraint (from within getInferredType) we eventually simplify this to U and to its base constraint. So to some extent, this leads to the same thing and the same problems. To incorporate inferences of other type parameters I'd have to utilize context.nonFixingMapper.

It's not immediately obvious how to get access to this from within this function (inferReverseMappedType) but that certainly can be done. A bigger problem though is that inferReverseMappedType gets called at different times for object and array/tuple types - when dealing with the latter it's called eagerly~. So it gets called right from within inferFromTypes. This introduces differences in data availability between the object and the array/tuple case.

For instance, let's take a look at this:

declare function foo<T extends U[], U extends string | number | boolean>(
  b: {
    [K in keyof T]: (arg: T[K]) => void;
  },
  a: U,
): T;

declare const data: number | string;

const result = foo([(arg) => {}, () => {}], data);

Since inferReverseMappedType gets called eagerly here to create the reverse mapped tuple type it's not possible to observe the U's inferred type. That is different from the equivalent object variant, like:

declare function foo<
  T extends Record<PropertyKey, U>,
  U extends string | number | boolean,
>(
  b: {
    [K in keyof T]: (arg: T[K]) => void;
  },
  a: U,
): T;

declare const data: number | string;

const result = foo(
  {
    a: (arg) => {},
    b: () => {},
  },
  data,
);

This isn't exactly a new problem. I've encountered this already in the past at least once or twice.

I'd really like to solve this. It feels like a separate issue though - but then: what would be the acceptable state of this PR to get this one in?

  1. does anything have to be done to move this one forward?
  2. would making inferReverseMappedType aware of the inference context (to get access to its .nonFixingMapper) be enough?
  3. should the object/array/tuple be unified for the whole thing to work in the same predictable way?

@Andarist Andarist force-pushed the reverse-mapped-type-default-to-constraint branch from b3f2274 to a0d8bcc Compare January 5, 2024 22:30
@@ -26185,10 +26197,17 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const constraint = getConstraintOfTypeParameter(inference.typeParameter);
if (constraint) {
const instantiatedConstraint = instantiateType(constraint, context.nonFixingMapper);
// TODO: decide what to do about fallback type
if (inferredType && inferredType.flags & TypeFlags.Object && (inferredType as ObjectType).objectFlags & ObjectFlags.ReverseMapped) {
(inferredType as ReverseMappedType).inferenceMapper = context.nonFixingMapper;
Copy link
Member

Choose a reason for hiding this comment

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

Hm. A mutation like this is odd, especially if the context.compareTypes call causes reentrancy on the inferredType (different places in the comparison could witness different mappers with different results). Instead, having a non-fixing-mapper clone of the type seems prudent.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah yes, this mutation is just the best I was able to figure out so far when it comes to exposing this mapper to inferReverseMappedType. I meant to comment on that here but you beat me to it and looked here first :P

I could maintain this in some stack or something but OTOH the mutation felt simpler because it just makes it directly accessible at the call site that needs this. It looks odd, especially here, but it's because reverse mapped types just don't participate in the same code path as everything else - all of them are kinda going through their own inference passes, even though they somewhat originate in this "parent" pass (they are not linked to it though).

I was also thinking that perhaps instead of assigning a mapper I could just assign the instantiatedConstraint. In away, the instantiation that I do now in inferReverseMappedType is quite similar to potential getIndexedAccessType(instantiatedConstraint, propertyNameType). Would that alleviate the reentrancy concern at all?

Instead, having a non-fixing-mapper clone of the type seems prudent.

I'm afraid that I don't understand what should be cloned here and where the clone should be used. Could you elaborate on this?


One extra thing that I was thinking about. getInferredType can return cached but non-fixed results. Does it mean that by doing this thing here we might accidentally~ "fix" the properties of a non-fixed result? Or is it just not a concern because the reverse mapped object with those fixed properties doesn't survive inferredType clears so it would be recreated and its properties would get "fixed" again (but now with the new nonFixingMapper/instantiatedConstraint). I didn't yet have time to investigate how this behaves in practice.

Copy link
Member

Choose a reason for hiding this comment

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

I'm afraid that I don't understand what should be cloned here and where the clone should be used. Could you elaborate on this?

Have a copy of the reverse mapped type that stores the nonFixingMapper instead of the normal mapper (and probably have that reverse mapped type mutually reference the normal version of the type). (And include that variant-ness in the ID of the reverse mapped type, so they're recognized as separate types in the caches)

Or is it just not a concern because the reverse mapped object with those fixed properties doesn't survive inferredType clears so it would be recreated and its properties would get "fixed" again (but now with the new nonFixingMapper/instantiatedConstraint). I didn't yet have time to investigate how this behaves in practice.

Yes but no. Reverse mapped types are made in inference, so they can be pretty short-lived, but they're identified structurally - all reverse mapped types with the same source, target, and constraint types are the same reverse mapped types, even when they occur at differing places in inference. So even if it's initially short lived, it can be pulled back out later by another attempt to construct one over the same types. That's actually another issue with this as it currently stands - including the mapper in the type (...by including it in the properties via this honestly roundabout way - passing it into inferTypeForHomomorphicMappedType/createReverseMappedType should clarify things) should affect the type id, but getting a good structural id out of a mapper isn't simple (since function mappers exist aplenty). This means in this PR right now, reverse mapped types mangle one another - only the first inference context within which a reverse mapped type of a certain structure is used gets cached, which is not correct. I think that since reverse mapped types are made within an inference context, them keeping a reference to said context is probably reasonable, but we'll definitely need to check that potentially duplicating reverse mapped types like that isn't terrible for perf on real projects (since inference contexts (and mappers) are rather throwaway).

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 author
Development

Successfully merging this pull request may close these issues.

Reverse mapped types don't use their constraint types when no candidates are present unlike the regular type parameters
5 participants