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

Fixed inferred type widening with contextual types created by binding patterns #56875

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

Conversation

Andarist
Copy link
Contributor

@Andarist Andarist commented Dec 25, 2023

@andrewbranch concluded here that:

While it may be true that object binding patterns never contribute anything useful to type argument inference, it is not true that array binding patterns always contribute usefully to type argument inference. What we really want is for binding patterns to contribute contextual typing information to other inference sources, while never being allowed to stand on their own as inference sources.

While working on this PR here, I realized that even object binding patterns can contribute useful things to inferences. That relies on initializers' types though, such as here (a test case from this PR):

declare function id<T>(x: T): T;
const { f = (x: string) => x.length } = id({ f: x => x.charAt });

One extra thing that I noticed based on the issue here is that constraints are still useful. When inferring from a binding pattern we often end up with an inferred type like { a: nonInferrableAny } but that doesn't pass the constraint check in getInferredType in the situations like this:

declare function id<T extends { a: 'foo' | 'bar', b: 'foo' | 'bar' }>(x: T): T;
const { a } = id({ a: 'foo', b: 'foo' });

The constraint here requires 2 properties but the type inferred from the binding pattern has only one. This particular case isn't problematic because when the inferred type doesn't satisfy the constraint the algorithm (silently~) picks the constraint.

Let's take a look at what happens in the reported case:

type R = { A: { a: 'A' } | null | undefined };
declare function fn<T extends R>(t: T): T;
const { A } = fn({ A: { a: 'A' } })

The inferred type here ({ A: nonInferrableAny; }) does satisfy the constraint and that leads to some lossy behaviors.

  1. we end up using the inferred type as contextual type and the next inference pass widens the literal string in getWidenedLiteralLikeTypeForContextualType to string
  2. then the inferred { A: string } fails the constraint check in getInferredType
  3. so we pick up the constraint and the argument satisfies that

The problem is that now the inferred type is the constraint (and that's nullable). We could have infer much better type of { A: { a: "A" } }.

This indicates that the constraint is still useful but our inferred type from the binding patterns might even be a supertype of the constraint (a binding pattern might only refer to some of the keys etc).

So the solution to this problem is to combine both pieces of information - the inferred type and the constraint type. Without introducing any new concepts to the compiler we can do that by intersecting them. That creates a problem without further changes since nonInferrablyAny is just any and T & any is just any. Using this is lossy for contextual typing in a similar way to the one described above.

On the other hand, unknown behaves almost perfectly for this purpose. It's "erasable" in intersections: T & unknown // unknown. The only real problem I have found with it is that intersections between objects with concrete properties and objects with index signatures don't behave as I'd like them to here.

A concrete property always shadows over the index signature - which is especially annoying~ here when it comes to tuple/array types:

type T1 = { a: nonInferrableUnknown } & { [k: string]: 'a' | 'b' }
type T2 = [nonInferrableUnknown] & ('a' | 'b')[]

In both of those situations we want to avoid those concrete properties to overshadow the more useful information contained in the index signature-based constraint. Since nonInferrableUnknown doesn't really hold any useful information for contextual typing it seems to be OK to ignore it and to look into the index signature in such cases. That has been implemented here.

fixes #56771
fixes #42969

Comment on lines +11449 to +11450
// widening of the binding pattern type substitutes a regular any for the non-inferrable unknown.
return includePatternInType ? nonInferrableUnknownType : anyType;
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'm not sure when "widening of the binding pattern type substitutes a regular any for the non-inferrable any" is happening and if this part of the comment is even relevant today.

@@ -20905,7 +20905,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
function isSimpleTypeRelatedTo(source: Type, target: Type, relation: Map<string, RelationComparisonResult>, errorReporter?: ErrorReporter) {
const s = source.flags;
const t = target.flags;
if (t & TypeFlags.Any || s & TypeFlags.Never || source === wildcardType) return true;
if (t & TypeFlags.Any || s & TypeFlags.Never || source === wildcardType || source === nonInferrableUnknownType) return true;
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 likely isn't strictly needed - at least not for the test cases I have here now. While iterating on this, I planned to make the created pattern type pass the constraint check in getInferredType. This line would allow for that in some scenarios. Later I realized that it wouldn't allow for that in all scenarios though - and that's why I just avoid doing the constraint check in getInferredType. The idea is still that the nonInferrableUnknown should act like any for assignability purposes though - I just replaced its type with unknown to benefit from the T & unknown // T.

@@ -25089,8 +25108,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}

function tupleTypesDefinitelyUnrelated(source: TupleTypeReference, target: TupleTypeReference) {
return !(target.target.combinedFlags & ElementFlags.Variadic) && target.target.minLength > source.target.minLength ||
!target.target.hasRestElement && (source.target.hasRestElement || target.target.fixedLength < source.target.fixedLength);
return !source.pattern && (!(target.target.combinedFlags & ElementFlags.Variadic) && target.target.minLength > source.target.minLength ||
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 allows us to infer between tuples of different shapes in those scenarios - since the tuple created by an array binding pattern might be shorter than the target. So far, I haven't managed to break this but I would advise extra caution around this when reviewing.

return undefined;
}
const type = getTypeOfSymbol(prop);
if (type !== nonInferrableUnknownType) {
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 nonInferrableUnknownType doesn't hold any useful information for contextual typing I let it fallthrough to index signatures here. Note that { x: nonInferrableUnknownType } & { x: string } shouldn't return nonInferrableUnknownType for 'x' property here. So if the constraint already defines a property then index signatures won't be consulted.

Comment on lines +30317 to +30319
if (t.flags & TypeFlags.Intersection) {
t = getIntersectionType((t as IntersectionType).types.filter(t => !t.pattern));
}
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 wish I knew how to do it in a better way... this is mainly to satisfy the isTupleType(t) below (when it should be satisfied)

var intrinsicMarkerType = createIntrinsicType(TypeFlags.Any, "intrinsic");
var unknownType = createIntrinsicType(TypeFlags.Unknown, "unknown");
var nonNullUnknownType = createIntrinsicType(TypeFlags.Unknown, "unknown", /*objectFlags*/ undefined, "non-null");
var nonInferrableUnknownType = createIntrinsicType(TypeFlags.Unknown, "unknown", ObjectFlags.ContainsWideningType, "non-inferrable");
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Likely getIntersectionType should reduce nonInferrableUnknown & unknown to unknown. Most of such reductions are performed based on includes coming from addTypesToIntersection. It would be cool to introduce TypeFlags.IncludesNonInferrableUnknown but those Includes* flags are mostly reusing (?) other existing flags and I'm not super comfortable with touching this on my own.

@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 Dec 26, 2023

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

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Dec 26, 2023

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

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Dec 26, 2023

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

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Dec 26, 2023

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

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Dec 26, 2023

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

@typescript-bot
Copy link
Collaborator

typescript-bot commented Dec 26, 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/159158/artifacts?artifactName=tgz&fileId=2179DCA59A835F70169F165ACDE539871D12513C7B89AD830445B4335B545D3102&fileName=/typescript-5.4.0-insiders.20231226.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.4.0-pr-56875-6".;

@typescript-bot
Copy link
Collaborator

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

There were infrastructure failures potentially unrelated to your change:

  • 1 instance of "Package install failed"

Otherwise...

Something interesting changed - please have a look.

Details

puppeteer

packages/browsers/test/src/tsconfig.json

@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,437k (± 0.01%) 295,440k (± 0.01%) ~ 295,402k 295,491k p=0.810 n=6
Parse Time 2.65s (± 0.28%) 2.65s (± 0.24%) ~ 2.64s 2.66s p=0.718 n=6
Bind Time 0.82s (± 0.50%) 0.82s (± 1.25%) ~ 0.81s 0.84s p=0.924 n=6
Check Time 8.13s (± 0.18%) 8.16s (± 0.15%) +0.03s (+ 0.35%) 8.15s 8.18s p=0.009 n=6
Emit Time 7.09s (± 0.19%) 7.11s (± 0.23%) +0.02s (+ 0.28%) 7.09s 7.13s p=0.050 n=6
Total Time 18.69s (± 0.15%) 18.74s (± 0.15%) +0.05s (+ 0.29%) 18.71s 18.78s p=0.019 n=6
Compiler-Unions - node (v18.15.0, x64)
Memory used 191,490k (± 0.03%) 194,422k (± 1.63%) ~ 191,461k 197,355k p=0.108 n=6
Parse Time 1.36s (± 0.86%) 1.35s (± 1.86%) ~ 1.32s 1.39s p=0.169 n=6
Bind Time 0.72s (± 0.00%) 0.72s (± 0.00%) ~ 0.72s 0.72s p=1.000 n=6
Check Time 9.26s (± 0.13%) 9.27s (± 0.26%) ~ 9.23s 9.30s p=0.625 n=6
Emit Time 2.62s (± 0.92%) 2.60s (± 0.31%) ~ 2.59s 2.61s p=0.357 n=6
Total Time 13.96s (± 0.20%) 13.94s (± 0.22%) ~ 13.90s 13.98s p=0.170 n=6
Monaco - node (v18.15.0, x64)
Memory used 347,395k (± 0.01%) 347,394k (± 0.01%) ~ 347,366k 347,430k p=0.873 n=6
Parse Time 2.46s (± 0.40%) 2.46s (± 0.47%) ~ 2.45s 2.48s p=0.864 n=6
Bind Time 0.92s (± 0.56%) 0.92s (± 0.56%) ~ 0.92s 0.93s p=1.000 n=6
Check Time 6.88s (± 0.58%) 6.88s (± 0.64%) ~ 6.82s 6.95s p=1.000 n=6
Emit Time 4.03s (± 0.41%) 4.08s (± 0.91%) +0.04s (+ 0.99%) 4.04s 4.14s p=0.040 n=6
Total Time 14.30s (± 0.23%) 14.34s (± 0.43%) ~ 14.28s 14.43s p=0.228 n=6
TFS - node (v18.15.0, x64)
Memory used 302,728k (± 0.00%) 302,737k (± 0.01%) ~ 302,711k 302,757k p=0.296 n=6
Parse Time 2.00s (± 1.24%) 1.99s (± 0.75%) ~ 1.97s 2.01s p=0.802 n=6
Bind Time 1.00s (± 0.51%) 1.00s (± 0.41%) ~ 0.99s 1.00s p=0.114 n=6
Check Time 6.31s (± 0.70%) 6.32s (± 0.34%) ~ 6.29s 6.35s p=0.517 n=6
Emit Time 3.58s (± 0.45%) 3.57s (± 0.69%) ~ 3.53s 3.60s p=0.744 n=6
Total Time 12.90s (± 0.45%) 12.88s (± 0.20%) ~ 12.84s 12.92s p=0.744 n=6
material-ui - node (v18.15.0, x64)
Memory used 506,845k (± 0.01%) 506,832k (± 0.00%) ~ 506,797k 506,860k p=0.575 n=6
Parse Time 2.59s (± 0.72%) 2.58s (± 0.47%) ~ 2.57s 2.60s p=0.461 n=6
Bind Time 0.99s (± 0.52%) 0.99s (± 1.51%) ~ 0.97s 1.01s p=0.672 n=6
Check Time 16.93s (± 0.37%) 16.91s (± 0.45%) ~ 16.79s 17.01s p=0.572 n=6
Emit Time 0.00s (± 0.00%) 0.00s (± 0.00%) ~ 0.00s 0.00s p=1.000 n=6
Total Time 20.52s (± 0.40%) 20.48s (± 0.34%) ~ 20.38s 20.58s p=0.423 n=6
xstate - node (v18.15.0, x64)
Memory used 512,898k (± 0.01%) 512,915k (± 0.01%) ~ 512,841k 512,992k p=0.630 n=6
Parse Time 3.28s (± 0.23%) 3.28s (± 0.23%) ~ 3.27s 3.29s p=1.000 n=6
Bind Time 1.53s (± 0.00%) 1.54s (± 0.36%) ~ 1.53s 1.54s p=0.071 n=6
Check Time 2.82s (± 0.18%) 2.82s (± 0.62%) ~ 2.79s 2.83s p=0.557 n=6
Emit Time 0.07s (± 0.00%) 0.07s (± 5.69%) ~ 0.07s 0.08s p=0.405 n=6
Total Time 7.70s (± 0.05%) 7.70s (± 0.33%) ~ 7.66s 7.74s p=0.220 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,349ms (± 0.56%) 2,351ms (± 0.97%) ~ 2,309ms 2,370ms p=0.630 n=6
Req 2 - geterr 5,426ms (± 1.36%) 5,484ms (± 1.26%) ~ 5,392ms 5,544ms p=0.230 n=6
Req 3 - references 324ms (± 1.71%) 328ms (± 1.73%) ~ 320ms 334ms p=0.418 n=6
Req 4 - navto 279ms (± 1.05%) 276ms (± 1.16%) ~ 273ms 280ms p=0.096 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 85ms (± 4.95%) 90ms (± 4.12%) ~ 83ms 93ms p=0.094 n=6
CompilerTSServer - node (v18.15.0, x64)
Req 1 - updateOpen 2,461ms (± 1.12%) 2,483ms (± 0.57%) ~ 2,460ms 2,500ms p=0.230 n=6
Req 2 - geterr 4,161ms (± 2.00%) 4,165ms (± 2.13%) ~ 4,068ms 4,255ms p=1.000 n=6
Req 3 - references 338ms (± 1.12%) 340ms (± 1.64%) ~ 333ms 348ms p=0.574 n=6
Req 4 - navto 283ms (± 0.41%) 285ms (± 1.16%) ~ 283ms 292ms p=0.117 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 82ms (± 7.14%) 84ms (± 6.73%) ~ 77ms 89ms p=0.553 n=6
xstateTSServer - node (v18.15.0, x64)
Req 1 - updateOpen 2,595ms (± 0.65%) 2,598ms (± 0.53%) ~ 2,577ms 2,615ms p=0.936 n=6
Req 2 - geterr 1,708ms (± 2.19%) 1,733ms (± 1.48%) ~ 1,683ms 1,753ms p=0.066 n=6
Req 3 - references 113ms (± 9.74%) 119ms (± 7.06%) ~ 102ms 123ms p=0.410 n=6
Req 4 - navto 366ms (± 0.91%) 366ms (± 0.61%) ~ 364ms 370ms p=0.505 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 310ms (± 1.52%) 312ms (± 1.08%) ~ 306ms 316ms p=0.627 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 153.32ms (± 0.21%) 153.29ms (± 0.19%) ~ 152.16ms 155.41ms p=0.808 n=600
tsserver-startup - node (v18.15.0, x64)
Execution time 228.55ms (± 0.15%) 228.15ms (± 0.14%) -0.40ms (- 0.17%) 226.76ms 232.20ms p=0.000 n=600
tsserverlibrary-startup - node (v18.15.0, x64)
Execution time 229.91ms (± 0.18%) 230.00ms (± 0.19%) ~ 228.31ms 234.98ms p=0.090 n=600
typescript-startup - node (v18.15.0, x64)
Execution time 230.34ms (± 0.18%) 230.20ms (± 0.17%) -0.15ms (- 0.06%) 228.41ms 233.11ms p=0.000 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.

@typescript-bot
Copy link
Collaborator

@jakebailey Here are the results of running the top-repos suite comparing main and refs/pull/56875/merge:

Everything looks good!

@sandersn sandersn added this to Not started in PR Backlog Jan 2, 2024
@sandersn sandersn moved this from Not started to Waiting on reviewers in PR Backlog Jan 3, 2024
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
PR Backlog
  
Waiting on reviewers
4 participants