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

Get apparent type for calls used directly at argument positions to aid nested inferences #52866

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

Conversation

Andarist
Copy link
Contributor

@Andarist Andarist commented Feb 20, 2023

fixes #52864
fixes #57978
competes with #52865

@typescript-bot typescript-bot added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Feb 20, 2023
Comment on lines 32144 to 32145
const argumentApparentType = outerContext && isCallOrNewExpression(node.parent) && getDiscriminatedApparentType(node, contextualType);
const instantiatedType = instantiateType(argumentApparentType && argumentApparentType !== unknownType ? argumentApparentType : contextualType, outerMapper);
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 is a different take on the problem that the one in #52865 .

The argumentApparentType !== unknownType is really meant to be isNotDefaultConstraintOfUnconstrainedTypeParamere(argumentApparentType). I thought that something like this would already exist but I couldn't find it. it's likely that without the strict null checks, we'd have to check against emptyObjectType. That's also why I temporarily added @strict here:
https://github.com/microsoft/TypeScript/pull/52866/files#diff-0ed2dcde3e5999198b7eebb67d0c508dbd4497536f956829f74706f33eca470dR3

@jakebailey
Copy link
Member

@typescript-bot test this
@typescript-bot test top100
@typescript-bot user test this
@typescript-bot run dt
@typescript-bot pack this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Feb 20, 2023

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

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Feb 20, 2023

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

@typescript-bot
Copy link
Collaborator

typescript-bot commented Feb 20, 2023

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

@typescript-bot
Copy link
Collaborator

typescript-bot commented Feb 20, 2023

Heya @jakebailey, I've started to run the extended test suite on this PR at 5d63a08. You can monitor the build here.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Feb 20, 2023

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

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Feb 20, 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/147023/artifacts?artifactName=tgz&fileId=BF87BB4DBE3F07C92E5E1A1D23030AACC50B3B848947DD1620C486EAF53831A502&fileName=/typescript-5.0.0-insiders.20230220.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.0.0-pr-52866-6".;

@typescript-bot
Copy link
Collaborator

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

Everything looks good!

@typescript-bot
Copy link
Collaborator

Heya @jakebailey, I've run the RWC suite on this PR - assuming you're on the TS core team, you can view the resulting diff here.

@typescript-bot
Copy link
Collaborator

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

Something interesting changed - please have a look.

Details

microsoft/vscode

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

src/tsconfig.monaco.json

src/tsconfig.tsec.json

@sandersn sandersn added this to Not started in PR Backlog Feb 28, 2023
@typescript-bot typescript-bot added For Backlog Bug PRs that fix a backlog bug and removed For Uncommitted Bug PR for untriaged, rejected, closed or missing bug labels Mar 3, 2023
@sandersn sandersn moved this from Not started to Waiting on reviewers in PR Backlog Mar 3, 2023
Copy link
Member

@weswigham weswigham left a comment

Choose a reason for hiding this comment

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

This is probably closer to a good fix than its' competitor PR, since our contextual type instantiation choice is somewhat heuristic in nature, and skipping said instantiation is supposed to be a perf optimization according to nearby comments, not something with visible impact (even though it definitely has visible impact in some cases, like the linked issue, and I know of others). The interplay of contextual type (instantiation) and return type inference is particularly complex, so I'd understand that adjusting something there is the way to go.

However, that said, what I don't get is the change to do discrimination of the contextual type, when, to what I can see, the motivating example doesn't have any discrimination in it. Is that an untested change that's also here for some reason? Is the key part of this change really just

const instantiatedType = instantiateType(getApparentType(contextualType) === unknownType ? contextualType : getApparentType(contextualType), outerMapper);

?
Because that feels kinda weird - like we should always get getting the apparent type of the contextual type or not, not this result-type-swapped middleground. Moreover, switching on unknownType in this context feels odd, too - what's privileged about a T extends unknown instead of a T extends number here (for example, if you swapped all your matcher examples to use a T extends string instead of an unconstrained T, would they still work)? I'm struggling to see the guiding principle behind the change.

PR Backlog automation moved this from Waiting on reviewers to Waiting on author Mar 14, 2023
@Andarist
Copy link
Contributor Author

I would have to refresh my memory on this one but I mentioned my intention behind this unknown check in the comment here. From what I understand, this was not supposed to be the final check - more like a stopgap to showcase the approach and get some feedback from the team. How does that explanation from the comment sound? If I read my own words correctly this was meant to check if we have a meaningful contextual type, something that can contribute further (?) to the algorithm

…nstantiated-from-the-outer-one-2

# Conflicts:
#	src/compiler/checker.ts
@Andarist
Copy link
Contributor Author

Andarist commented Jun 5, 2023

However, that said, what I don't get is the change to do discrimination of the contextual type, when, to what I can see, the motivating example doesn't have any discrimination in it. Is that an untested change that's also here for some reason?

This part doesn't have a test right now. The main idea behind this fix is to mimic what getApparentTypeOfContextualType does already. That's why I moved its part to a separate function (getDiscriminatedApparentType) and call that in my fix. At the same time, nested properties within the nested call already get the good contextual type (through the mentioned getApparentTypeOfContextualType) and that's why I have the isCallOrNewExpression(node.parent) check. If the node's parent is not the call/new expression (ye, I know, I should skipParentheses here) then I don't have to getDiscriminatedApparentType here because it will be called further down the road.

Is the key part of this change really just: const instantiatedType = instantiateType(getApparentType(contextualType) === unknownType ? contextualType : getApparentType(contextualType), outerMapper);

Not quite, the whole isCallOrNewExpression check is somewhat crucial here. With the quoted version we get such baseline failures:

baselines diff with the quoted version
diff --git a/tests/baselines/reference/callChain.3.errors.txt b/tests/baselines/reference/callChain.3.errors.txt
index 6deb1db1b4..2165107e1d 100644
--- a/tests/baselines/reference/callChain.3.errors.txt
+++ b/tests/baselines/reference/callChain.3.errors.txt
@@ -1,6 +1,6 @@
 tests/cases/conformance/expressions/optionalChaining/callChain/callChain.3.ts(3,7): error TS2322: Type 'number | undefined' is not assignable to type 'number'.
   Type 'undefined' is not assignable to type 'number'.
-tests/cases/conformance/expressions/optionalChaining/callChain/callChain.3.ts(4,7): error TS2322: Type 'number | undefined' is not assignable to type 'number'.
+tests/cases/conformance/expressions/optionalChaining/callChain/callChain.3.ts(4,7): error TS2322: Type 'Number | undefined' is not assignable to type 'number'.
   Type 'undefined' is not assignable to type 'number'.
 
 
@@ -13,7 +13,7 @@ tests/cases/conformance/expressions/optionalChaining/callChain/callChain.3.ts(4,
 !!! error TS2322:   Type 'undefined' is not assignable to type 'number'.
     const n2: number = a?.m?.({x: absorb()}); // likewise
           ~~
-!!! error TS2322: Type 'number | undefined' is not assignable to type 'number'.
+!!! error TS2322: Type 'Number | undefined' is not assignable to type 'number'.
 !!! error TS2322:   Type 'undefined' is not assignable to type 'number'.
     const n3: number | undefined = a?.m?.({x: 12}); // should be ok
     const n4: number | undefined = a?.m?.({x: absorb()}); // likewise
diff --git a/tests/baselines/reference/callChain.3.types b/tests/baselines/reference/callChain.3.types
index c4afc572ab..c1ce3b91d5 100644
--- a/tests/baselines/reference/callChain.3.types
+++ b/tests/baselines/reference/callChain.3.types
@@ -20,13 +20,13 @@ const n1: number = a?.m?.({x: 12 }); // should be an error (`undefined` is not a
 
 const n2: number = a?.m?.({x: absorb()}); // likewise
 >n2 : number
->a?.m?.({x: absorb()}) : number | undefined
+>a?.m?.({x: absorb()}) : Number | undefined
 >a?.m : (<T>(obj: { x: T; }) => T) | undefined
 >a : { m?<T>(obj: { x: T; }): T; } | undefined
 >m : (<T>(obj: { x: T; }) => T) | undefined
->{x: absorb()} : { x: number; }
->x : number
->absorb() : number
+>{x: absorb()} : { x: Number; }
+>x : Number
+>absorb() : Number
 >absorb : <T>() => T
 
 const n3: number | undefined = a?.m?.({x: 12}); // should be ok

On top of that, we also get an AssertionError in one of the tests related to quick info:

         fourslash tests
           tests/cases/fourslash/quickInfoGenericCombinators2.ts
             fourslash test quickInfoGenericCombinators2.ts runs correctly:
     AssertionError: At marker '13': quick info text: expected 'var r3a: Collection<number, {}>' to equal 'var r3a: Collection<number, unknown>'

Basically, it seems that for nested properties~ this call to getApparentType is just too eager/happens too early.

Because that feels kinda weird - like we should always get getting the apparent type of the contextual type or not, not this result-type-swapped middleground.

If we'd just replace this with:

const instantiatedType = instantiateType(getApparentType(contextualType), outerMapper)

then we'd end up with the same quick info failure and those changed baselines (similar but not the same as the previous ones):

baselines diff with non-conditional `getApparentType` call
diff --git a/tests/baselines/reference/callChain.3.errors.txt b/tests/baselines/reference/callChain.3.errors.txt
index 6deb1db1b4..e93c130238 100644
--- a/tests/baselines/reference/callChain.3.errors.txt
+++ b/tests/baselines/reference/callChain.3.errors.txt
@@ -1,10 +1,10 @@
 tests/cases/conformance/expressions/optionalChaining/callChain/callChain.3.ts(3,7): error TS2322: Type 'number | undefined' is not assignable to type 'number'.
   Type 'undefined' is not assignable to type 'number'.
-tests/cases/conformance/expressions/optionalChaining/callChain/callChain.3.ts(4,7): error TS2322: Type 'number | undefined' is not assignable to type 'number'.
-  Type 'undefined' is not assignable to type 'number'.
+tests/cases/conformance/expressions/optionalChaining/callChain/callChain.3.ts(4,7): error TS2322: Type 'unknown' is not assignable to type 'number'.
+tests/cases/conformance/expressions/optionalChaining/callChain/callChain.3.ts(6,7): error TS2322: Type 'unknown' is not assignable to type 'number | undefined'.
 
 
-==== tests/cases/conformance/expressions/optionalChaining/callChain/callChain.3.ts (2 errors) ====
+==== tests/cases/conformance/expressions/optionalChaining/callChain/callChain.3.ts (3 errors) ====
     declare function absorb<T>(): T;
     declare const a: { m?<T>(obj: {x: T}): T } | undefined;
     const n1: number = a?.m?.({x: 12 }); // should be an error (`undefined` is not assignable to `number`)
@@ -13,10 +13,11 @@ tests/cases/conformance/expressions/optionalChaining/callChain/callChain.3.ts(4,
 !!! error TS2322:   Type 'undefined' is not assignable to type 'number'.
     const n2: number = a?.m?.({x: absorb()}); // likewise
           ~~
-!!! error TS2322: Type 'number | undefined' is not assignable to type 'number'.
-!!! error TS2322:   Type 'undefined' is not assignable to type 'number'.
+!!! error TS2322: Type 'unknown' is not assignable to type 'number'.
     const n3: number | undefined = a?.m?.({x: 12}); // should be ok
     const n4: number | undefined = a?.m?.({x: absorb()}); // likewise
+          ~~
+!!! error TS2322: Type 'unknown' is not assignable to type 'number | undefined'.
     
     // Also a test showing `!` vs `?` for good measure
     let t1 = a?.m?.({x: 12});
diff --git a/tests/baselines/reference/callChain.3.types b/tests/baselines/reference/callChain.3.types
index c4afc572ab..0167fb3e9f 100644
--- a/tests/baselines/reference/callChain.3.types
+++ b/tests/baselines/reference/callChain.3.types
@@ -20,13 +20,13 @@ const n1: number = a?.m?.({x: 12 }); // should be an error (`undefined` is not a
 
 const n2: number = a?.m?.({x: absorb()}); // likewise
 >n2 : number
->a?.m?.({x: absorb()}) : number | undefined
+>a?.m?.({x: absorb()}) : unknown
 >a?.m : (<T>(obj: { x: T; }) => T) | undefined
 >a : { m?<T>(obj: { x: T; }): T; } | undefined
 >m : (<T>(obj: { x: T; }) => T) | undefined
->{x: absorb()} : { x: number; }
->x : number
->absorb() : number
+>{x: absorb()} : { x: unknown; }
+>x : unknown
+>absorb() : unknown
 >absorb : <T>() => T
 
 const n3: number | undefined = a?.m?.({x: 12}); // should be ok
@@ -41,13 +41,13 @@ const n3: number | undefined = a?.m?.({x: 12}); // should be ok
 
 const n4: number | undefined = a?.m?.({x: absorb()}); // likewise
 >n4 : number | undefined
->a?.m?.({x: absorb()}) : number | undefined
+>a?.m?.({x: absorb()}) : unknown
 >a?.m : (<T>(obj: { x: T; }) => T) | undefined
 >a : { m?<T>(obj: { x: T; }): T; } | undefined
 >m : (<T>(obj: { x: T; }) => T) | undefined
->{x: absorb()} : { x: number; }
->x : number
->absorb() : number
+>{x: absorb()} : { x: unknown; }
+>x : unknown
+>absorb() : unknown
 >absorb : <T>() => T
 
 // Also a test showing `!` vs `?` for good measure
diff --git a/tests/baselines/reference/inferFromBindingPattern.types b/tests/baselines/reference/inferFromBindingPattern.types
index ee217fafa8..b5f8e0790f 100644
--- a/tests/baselines/reference/inferFromBindingPattern.types
+++ b/tests/baselines/reference/inferFromBindingPattern.types
@@ -95,16 +95,16 @@ declare function stringy<T = string>(arg?: T): T;
 >arg : T | undefined
 
 const isStringTuple = makeTuple(stringy());  // [string]
->isStringTuple : [string]
->makeTuple(stringy()) : [string]
+>isStringTuple : [unknown]
+>makeTuple(stringy()) : [unknown]
 >makeTuple : <T1>(arg: T1) => [T1]
->stringy() : string
+>stringy() : unknown
 >stringy : <T = string>(arg?: T | undefined) => T
 
 const [isAny] = makeTuple(stringy());  // [string]
->isAny : string
->makeTuple(stringy()) : [string]
+>isAny : unknown
+>makeTuple(stringy()) : [unknown]
 >makeTuple : <T1>(arg: T1) => [T1]
->stringy() : string
+>stringy() : unknown
 >stringy : <T = string>(arg?: T | undefined) => T

Moreover, switching on unknownType in this context feels odd, too - what's privileged about a T extends unknown instead of a T extends number here

It's not that T extends unknown is privileged, it's rather that it's not privileged. It doesn't provide any new information that could be usef for improved contextual typing here.

for example, if you swapped all your matcher examples to use a T extends string instead of an unconstrained T, would they still work

Yes.

I'm struggling to see the guiding principle behind the change.

It's just a product of my experiments with this code, primarily done by looking at other, similar, cases, comparing them and fine-tuning this call site. It's not exactly that I have any "guiding principle" behind this change 😉 it just tries to mimick getApparentTypeOfContextualType.


Note that currently this PR breaks VS Code's codebase, I managed to extract a few failing test cases based on the bot's results: TS playground. Every other caught case looks pretty much the same so I think that those 3 are representative - gonna investigate this soon.

@Andarist Andarist requested a review from weswigham June 5, 2023 15:27
@Andarist
Copy link
Contributor Author

Andarist commented Jun 6, 2023

I've done further debugging to understand what's happening with the breaks in VS Code's codebase:

  1. those are not exactly sound, especially the one related to querying the DOM elements. All of them rely on the contextualType from the explicit annotation on the target's assignment (like const textArea: HTMLTextAreaElement = outer(inner())). I think though that this kind of unsoundness is acceptable here. It's impossible to distinguish this case from a factory function and I think that you want this to work: const onDidChangePassword: Emitter<ICredentialsChangeEvent> = register(new Emitter())
  2. the case that is being fixed here doesn't have a contextualType for the outer call though. In this case, we don't witness the outer candidates before getting to resolveCall for the inner call.

Perhaps part of the fix should be to somehow determine the correct behavior based on the existence of the outer contextualType or based on the presence of outerContext.returnMapper?

…nstantiated-from-the-outer-one-2

# Conflicts:
#	src/compiler/checker.ts
@fabian-hiller
Copy link

Would love to see this merged to fix #57978

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
PR Backlog
  
Waiting on author
5 participants