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

Infer variables used in nested function are not null if already proven not null in parent function #32339

Closed
5 tasks done
devuxer opened this issue Jul 10, 2019 · 7 comments
Closed
5 tasks done
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@devuxer
Copy link

devuxer commented Jul 10, 2019

Search Terms

  • nested function

Suggestion

Allow this to compile without error:

const x: number | null = 3;

function outer() {
    if (!x) return null;
    return inner();

    function inner() {
        return x + 3;
    }
}

Currently, the compiler complains that the x in x + 3 could be null, even though that check was performed in the parent function.

View in playground

Use Cases

  • Eliminate need to check the same variable for null twice within the same scope or use bang (!) operator unnecessarily.

Examples

(see above)

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@jcalz
Copy link
Contributor

jcalz commented Jul 11, 2019

Duplicate of #9998?

@fatcerberus
Copy link

fatcerberus commented Jul 11, 2019

@jcalz Nah, it's a little different I'm pretty sure - #9998 is a design limitation mostly to do with whether narrowings should be preserved across function calls. In the case here, a narrowed type is being re-widened at a closure boundary, which is AFAIK fully intentional and by design. The function call itself has nothing (directly) to do with it.

Either way though, this is almost certainly a wontfix.

@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Jul 11, 2019
@RyanCavanaugh
Copy link
Member

One of the principles of predictability (and performance) is that the checking of a function body doesn't change based on when or how the function is called.

This kind of behavior would only be useful and performant for functions which are called exactly once, which are the kind of functions that don't need to exist in the first place.

@devuxer
Copy link
Author

devuxer commented Jul 11, 2019

@RyanCavanaugh,

Thanks for the explanation...interesting.

So, strangely, but I think in accordance with what you're saying, the compiler doesn't actually have a problem with this (however, it fails when run).

outer();

function outer() {
    const result = inner();
    const x = 3;
    console.log(result);

    function inner() {
        return x + 3;
    }
}

This kind of behavior would only be useful and performant for functions which are called exactly once, which are the kind of functions that don't need to exist in the first place.

I'm having trouble understanding this part. Why does the number of times a function is called matter? And when you say "performant", are you talking about the speed at which the code runs or the speed at which the code is compiled?

Here's an example where the nested function is called multiple times. It doesn't compile for the same reason as in my original example.

outer(3, [1, 2, 3]);

function outer(x: number | null, arr: number[]) {
    if (!x) return;
    arr.forEach(callbackFn)

    function callbackFn(num: number) {
        console.log(x + num);
    }
}

In what sense are performance and predictability different in this case vs. callbackFn being called only once?

Edit

Interestingly, the arrow function version of this does compile.

outer(3, [1, 2, 3]);

function outer(x: number | null, arr: number[]) {
    if (!x) return;
    arr.forEach(num => { console.log(x + num) })
}

Now I'm really confused :)

@fatcerberus
Copy link

Yes, arrow functions are expressions so they are part of the control flow graph. Function declarations on the other hand get hoisted so they are completely outside the CF graph of the containing scope, see discussion about that here: #32300

@devuxer
Copy link
Author

devuxer commented Jul 12, 2019

@fatcerberus,

Ah, it does make sense that function expressions (regardless of whether arrow) get evaluated differently than function declarations.

@devuxer devuxer closed this as completed Jul 12, 2019
@typescript-bot
Copy link
Collaborator

This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

5 participants