Skip to content

Conversation

@sandersn
Copy link
Contributor

@sandersn sandersn commented Sep 14, 2023

Broken by improved overload inference in microsoft/TypeScript#54448

Summary

Using PromiseLike or JQuery.Thenable (an alias) now correctly propagates an any from the onrejected parameter to subsequent promises like so:

const q = p.then(() => thenable, () => promise3)
q.then(
  (a,b,c) => {} // ok
  (a, b, c) => {} // a: any because `thenable.onrejected's first parameter is any =(
  (a, b, c) => {} // ok
)

TS 5.2 and earlier dropped inference information between overloaded signatures. JQuery relies on this in an extremely subtle way—for thenable, inferences came from the last overload of JQuery.PromiseBase.then, even though the correct overload is actually the 5th one. And now that all the inferences are coming through in TS 5.3, the any in the stdlib's PromiseLike.then failure continuation has come back to bite us.

The any is built-in to the standard lib, so anybody that uses PromiseLike or JQuery.Thenable will lose type information in subsequent promises. In the example, the any from thenable overrides type information from promise3 because any | T1 | T2 | ... = any

Action

Change the tests to reflect the new 5.3 behaviour. It's unfortunate for jquery, but correct.

I haven't tried selectively deleting Thenable from PromiseBase.then parameters, but that might work. But it's likely to break far more code and slow down compilation as the compiler falls back to structural comparison between Thenable and PromiseLike.
Edit: No, the inference is the same, and breaks some uses of PromiseBase.then.

Explanation?!

Let's see if I can explain this in more detail:

Before the linked PR, Typescript incorrectly inferred between signatures with different numbers of overloads. If you had a target signature with 3 overloads A1, B1, C1 and source signature with two overloads B2, C2 it would look for inferences between

C2 -> C1
B2 -> B1

and then it would stop. Any possible inferences to A1 would be lost. After the linked PR, the inferences look like this:

C2 -> C1
B2 -> B1
B2 -> A1

which means that any type variables that are only in A1 may not get infererences. Why does this matter to JQuery? Well, the more common case is a single-overload source signature inferring to a multiple-overload target signature. And that's what we have with then:

                const q = p.then(() => {
                    return t1;
                }, () => {
                    return p2;
                });

                q.then((a, b, c) => {
                    a; // $ExpectType J1 | J2
                    b; // $ExpectType J5
                    c; // $ExpectType J8
                }, (a, b, c) => {
                    a; // $ExpectType J3 || any
                    b; // $ExpectType J6
                    c; // $ExpectType J9
                }, (a, b, c) => {
                    a; // $ExpectType J4
                    b; // $ExpectType J7
                    c; // $ExpectType J1
                });

Here, t1: JQuery.Thenable<J1> and p2: JQuery.Promise3<J2, J3, J4, J5, J6, J7, J8, J9, J1>. p also has type Promise3, so failFilter, the second parameter, has perfectly matched overloads to infer between since both signatures are from Promise3.then. However, in doneFilter, the first parameter, t1: JQuery.Thenable<J1> only has one signature.

Before, the inference would look like this:

    PromiseLike.then<TResult1 = T, TResult2 = never>(
        onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, 
        onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): 
    PromiseLike<TResult1 | TResult2>;
->
        JQuery.PromiseBase.then<ARD = never, AJD = never, AND = never,
            BRD = never, BJD = never, BND = never,
            CRD = never, CJD = never, CND = never,
            RRD = never, RJD = never, RND = never>(
                doneFilter: (t: TR, u: UR, v: VR, ...s: SR[]) => PromiseBase<ARD, AJD, AND, BRD, BJD, BND, CRD, CJD, CND, RRD, RJD, RND> | Thenable<ARD> | ARD,
                failFilter?: null,
                progressFilter?: null): PromiseBase<ARD, AJD, AND, BRD, BJD, BND, CRD, CJD, CND, RRD, RJD, RND>;

Only one round of inference. Now, PromiseLike.then's single signature will be used as the source for all seven of PromiseBase.then's overloads.

Why is that a problem? Well, look at failFilter. In this overload, it's null. So no inferences will come from PromiseLike.onrejected. That's a good thing, because onrejected's has parameter reason: any. Instead, TS 5.2 would only get AJD=J3. Which is the desired inference for the test code I pasted above.

In TS 5.3, the compiler will run inference against the other six overloads as well. In particular, there's one where doneFilter and failFilter are both defined (the overload that actually gets chosen, in fact):

        then<ARD = never, AJD = never, AND = never,
            BRD = never, BJD = never, BND = never,
            CRD = never, CJD = never, CND = never,
            RRD = never, RJD = never, RND = never,
            ARF = never, AJF = never, ANF = never,
            BRF = never, BJF = never, BNF = never,
            CRF = never, CJF = never, CNF = never,
            RRF = never, RJF = never, RNF = never>(
                doneFilter: (...t: TR[]) => PromiseBase<ARD, AJD, AND, BRD, BJD, BND, CRD, CJD, CND, RRD, RJD, RND> | Thenable<ARD> | ARD,
                failFilter: (...t: TJ[]) => PromiseBase<ARF, AJF, ANF, BRF, BJF, BNF, CRF, CJF, CNF, RRF, RJF, RNF> | Thenable<ARF> | ARF,
                progressFilter?: null): 
        PromiseBase<ARD | ARF, AJD | AJF, AND | ANF, BRD | BRF, BJD | BJF, BND | BNF, CRD | CRF, CJD | CJF, CND | CNF, RRD | RRF, RJD | RJF, RND | RNF>;

Here, the type parameter for the first parameter of failFilter is AJD | AJF. But now PromiseLike.onrejected's reason: any is available as an inference for AJF, the inferences are AJD=J3, AJF=any, which means that the second parameter of the returned PromiseBase, AJD | AJF, gets instantiated as J3 | any....which is just any.

@typescript-bot
Copy link
Contributor

typescript-bot commented Sep 14, 2023

@sandersn Thank you for submitting this PR!

This is a live comment which I will keep updated.

1 package in this PR

Code Reviews

Because this is a widely-used package, a DT maintainer will need to review it before it can be merged.

You can test the changes of this PR in the Playground.

Status

  • ✅ No merge conflicts
  • ✅ Continuous integration tests have passed
  • 🕐 Most recent commit is approved by a DT maintainer

Once every item on this list is checked, I'll ask you for permission to merge and publish the changes.


Diagnostic Information: What the bot saw about this PR
{
  "type": "info",
  "now": "-",
  "pr_number": 66707,
  "author": "sandersn",
  "headCommitOid": "48c88572258787d6becb4a3689576fef1d7f49f8",
  "mergeBaseOid": "93480a775a6b0181fc0cf2ec5e3b5002d70fcaec",
  "lastPushDate": "2023-09-14T23:09:21.000Z",
  "lastActivityDate": "2023-09-15T05:07:12.000Z",
  "hasMergeConflict": false,
  "isFirstContribution": false,
  "tooManyFiles": false,
  "hugeChange": false,
  "popularityLevel": "Critical",
  "pkgInfo": [
    {
      "name": "jquery",
      "kind": "edit",
      "files": [
        {
          "path": "types/jquery/jquery-tests.ts",
          "kind": "test"
        }
      ],
      "owners": [
        "leonard-thieu",
        "borisyankov",
        "choffmeister",
        "Steve-Fenton",
        "Diullei",
        "tasoili",
        "seanski",
        "Guuz",
        "ksummerlin",
        "basarat",
        "nwolverson",
        "derekcicerone",
        "AndrewGaspar",
        "seikichi",
        "benjaminjackman",
        "JoshStrobl",
        "johnnyreilly",
        "DickvdBrink",
        "King2500",
        "terrymun",
        "martin-badin",
        "princefishthrower"
      ],
      "addedOwners": [],
      "deletedOwners": [],
      "popularityLevel": "Critical"
    }
  ],
  "reviews": [
    {
      "type": "approved",
      "reviewer": "martin-badin",
      "date": "2023-09-15T05:07:12.000Z",
      "isMaintainer": false
    }
  ],
  "mainBotCommentID": 1720269207,
  "ciResult": "pass"
}

@typescript-bot
Copy link
Contributor

🔔 @leonard-thieu @borisyankov @choffmeister @Steve-Fenton @Diullei @tasoili @seanski @Guuz @ksummerlin @basarat @nwolverson @derekcicerone @AndrewGaspar @seikichi @benjaminjackman @JoshStrobl @johnnyreilly @DickvdBrink @King2500 @terrymun @martin-badin @princefishthrower — please review this PR in the next few days. Be sure to explicitly select Approve or Request Changes in the GitHub UI so I know what's going on.

@typescript-bot typescript-bot added the Owner Approved A listed owner of this package signed off on the pull request. label Sep 15, 2023
@sandersn sandersn merged commit 7e53842 into master Sep 15, 2023
@sandersn sandersn deleted the update-jquery-tests-for-ts5.3 branch September 15, 2023 15:44
@sandersn
Copy link
Contributor Author

Thanks @martin-badin for reading through that explanation!

@johnnyreilly
Copy link
Member

Amazing! I think this should resolve the issues in #66531 - just need to sync up to master

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

Labels

Critical package Owner Approved A listed owner of this package signed off on the pull request.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants