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

Recursive async function return type not inferred when await on the self call #55973

Closed
miguel-leon opened this issue Oct 4, 2023 · 7 comments · Fixed by #56020
Closed

Recursive async function return type not inferred when await on the self call #55973

miguel-leon opened this issue Oct 4, 2023 · 7 comments · Fixed by #56020
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@miguel-leon
Copy link

🔎 Search Terms

async recursive await

🕗 Version & Regression Information

Latest version

⏯ Playground Link

https://www.typescriptlang.org/play?#code/PTAEEsDsDMFMCcDOoAuBPADrUB3cKALAewFcVQBDHC-AKAsTUgGNRoSWVwjJR5ZmARgAUASlABvWqBkRooYQFkKhAHTwKkACZEAtmNAAeUAAZVAVnFTZNvrBQl4vfkLEBuabIC+oWABtEbGtbGX4HJ1AAcgJ-PyJIjxsvWmTaEAgYBGRNNFx8AkpqOgYmVnZObmcBACYDYJlweSUVAnVNHX1xYzNLSU8QsMdeKhpyF1rRRO9fAKD+20GI6Nj4qZlk5KA

💻 Code

// infers type without await
async function rec1() {
    if (Math.random() < 0.5) {
        return rec1();
    } else {
        return 'hello';
    }
}

// infers any with await
async function rec2() {
    if (Math.random() < 0.5) {
        return await rec2();
    } else {
        return 'hello';
    }
}

🙁 Actual behavior

infers any with await on self call

🙂 Expected behavior

should infer the return type as it does without await

Additional information about the issue

To my understanding, the code has the exact same runtime behavior, so one would expect the same at compilation.

If working as intended? why?

Thanks.

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Oct 4, 2023

See also #53995

In the rec1 case, we can be sure that it's safe to "set aside" the rec1() call, since the correct return type will just be whatever the other return statements are.

In the rec2 case, that's not the case, because await has a nonzero effect on the type of the return. It's not a "tail" position.

@RyanCavanaugh RyanCavanaugh added the Design Limitation Constraints of the existing architecture prevent this from being fixed label Oct 4, 2023
@Andarist
Copy link
Contributor

Andarist commented Oct 5, 2023

Hmm, I was about to expand the logic from #53995 to cover for this. I struggle to find the reason why this couldn't be done here. Would you be able to illustrate what you mean by the nonzero effect on the return type here with some code snippet? Since the await is just passes through to return in rec2 it feels to me that the tail call simplification could apply here as well ("the correct return type will just be whatever the other return statements are.")

@RyanCavanaugh
Copy link
Member

I guess the thing that I have to be convinced of is that await foo() in async function foo() is necessarily going to be effect-free in terms of the effect of foo's return type. I think you're probably right about that since await on a Promise is a well-defined thing in terms of maintaining the inner type.

I'd want to see testcases like this

declare const ps: Promise<string> | number;

async function foo1() {
    if (Math.random() > 0.5) {
        return ps;
    } else {
        return await foo1();
    }
}

async function foo2() {
    if (Math.random() > 0.5) {
        return ps;
    } else {
        return foo2();
    }
}

to be fully convinced.

@fatcerberus
Copy link

fatcerberus commented Oct 5, 2023

I believe return await foo() is 100% equivalent to return foo() in terms of observable effects, except for the specific case where there’s a local catch or finally guarding it.

@craigphicks
Copy link

craigphicks commented Oct 7, 2023

I was wondering if the general case might need to be concerned about side effects, but I found that side effects are not accounted for even in a non async function:

declare const x: { a: 1|2 };
function f() {
    x.a = 2;
    return;
}
function rec1() {
    if (Math.random() < 0.5 && x.a===1) {
        f();
        return x.a;
    } else {
        return rec1();
    }
}
// - function rec1(): 1

function rec2(): 2 {
    if (Math.random() < 0.5 && x.a===1) {
        f();
        return x.a; // Type '1' is not assignable to type '2'.(2322)
    } else {
        return rec2();
    }
}

playground

Shouldn't it be true that any call out (e.g. f()) should re-widen all of the writable variables (including properties) external to the function?

Furthermore, shouldn't any await call within an async call also re-widen all of the writable variables (including properties) external to the function, because unknown functions with unknown side effects may run before resuming from await?

This OPs issue seems correct, it also seems to be about a very special narrow case, so care has to be taken any conclusions from this special case are not extended to cases to which they shouldn't be applied.

@Andarist
Copy link
Contributor

Andarist commented Oct 7, 2023

@fatcerberus "except for the specific case where there’s a local catch or finally guarding it.". That can't impact the return type at the return await position though, right? Those could just add other return statement positions - ones that wouldn't be "ignored", ones that would be gathered as normal when computing the inferred return type of the whole function.

@fatcerberus
Copy link

fatcerberus commented Oct 7, 2023

@Andarist Yeah, I'm not 100% sure but I think you're right. I know that writing return foo() prevents any local catch or finally block from executing if foo's promise rejects. But I think you're right that return await is never going to impact the potential return types of the function, unless perhaps you're doing something really weird with custom promises--and even then I'm not convinced it's possible to manifest an observable difference.

@craigphicks

Shouldn't it be true that any call out (e.g. f()) should re-widen all of the writable variables (including properties) external to the function?

See #9998 for elaboration on why that doesn't happen. Same rationale applies to side effects during processing of await. "Side effects" is probably too broad a term for the discussion here; what @RyanCavanaugh was specifically concerned about was whether a return await can impact the return type of the function compared to a plain return, but I don't believe that to be the case, as @Andarist above me points out.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
5 participants