microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.08k stars 12.37k forks source link

Infer variable type form return type #16218

Closed mweststrate closed 7 years ago

mweststrate commented 7 years ago

TypeScript Version: 2.3.2

In many cases I have a variable that holds a value to be eventually returned from the function. The type of this variable always matches the return type of the function. It would be awesome if the type of that variable can be inferred from the fact that it is returned, using return, from the function, if the function has an explicitly declared return type.

I am aware that inference is done the other way around, however, we always work with explicit return types on functions. We have linting rules enforcing this, as it is a good practice that helps code reviews and encourages api-first thinking.

Code

So currently I have to write

        function findReconcilationCandidates(snapshot: any): Node[] {
            const res: Node[] = []
            // push some items on res   
            return res
        }

Expected behavior:

        function findReconcilationCandidates(snapshot: any): Node[] {
            const res = [] // type is inferred from return statement
            // push some items on res   
            return res
        }

Actual behavior:

Compile error:

'Variable 'res' implicitly has type 'any[]' in some locations where its type cannot be determined.'

ghost commented 7 years ago

What if you wanted to do this?

function f(): ReadonlyArray<Animal> {
    const res = [];
    res.push(new Cat());
    res[0].meow();
    return res;
}

If we infer res: ReadonlyArray<Animal> you will get problems because internally it's a mutable array of Cats.

In the particular case you're looking at, we actually will infer the type of res correctly if it's an array that's pushed to, with no type annotations needed at all. See #11432.

function f() {
    const res = [];
    res.push(1);
    return res;
}

Here we infer f: () => number[].

DanielRosenwasser commented 7 years ago

In addition to Andy's point, this would be a pretty big change in our type system. We don't really do inference going "backwards" from latter statements. The type system works off of a declared type which is inferred from its declaration. That type is specialized by subsequent checks and assignments.

While there are the concepts of type argument inference and contextual types (which I mention because they have a sort of back-and-forth interaction), those are limited to the statement level.

mweststrate commented 7 years ago

@andy-ms your examples are unclear to me; as both will result in compile errors (with strict null checks enabled). With out them, the infered type of f is any[], not number[]. See this [playground examle](http://www.typescriptlang.org/play/index.html#src=class%20Animal%20%7B%0D%0A%20%20%0D%0A%7D%0D%0A%0D%0Aclass%20Cat%20%7B%0D%0A%20%20meow()%20%7B%0D%0A%20%20%20%20%0D%0A%20%20%7D%0D%0A%7D%0D%0A%0D%0Afunction%20f()%3A%20ReadonlyArray%3CAnimal%3E%20%7B%0D%0A%20%20%20%20const%20res%20%3D%20%5B%5D%3B%0D%0A%20%20%20%20res.push(new%20Cat())%3B%0D%0A%20%20%20%20res%5B0%5D.meow()%3B%0D%0A%20%20%20%20return%20res%3B%0D%0A%7D%0D%0A%0D%0Afunction%20f2()%20%7B%0D%0A%20%20%20%20const%20res%20%3D%20%5B%5D%3B%0D%0A%20%20%20%20res.push(1)%3B%0D%0A%20%20%20%20return%20res%3B%0D%0A%7D%0D%0A%0D%0Aconst%20a%20%3D%20f()%0D%0Aconst%20b%20%3D%20f2())

'Argument of type 'Cat' is not assignable to parameter of type 'never'.' 'Argument of type '1' is not assignable to parameter of type 'never'.'

EDIT: The examples (suprisinginly?) do not give compile errors if noImplicitAny is enabled

@DanielRosenwasser I can imagine this is hard to solve generically, but I figured it would be pretty convenient, especially where a variable type could be inferred directly from the return statements, and normal type inference cannot deliver a "good" candidate.

Not being able to infer a good candidate is probably limited to empty arrays and empty object literals:

This annoyance is especially noticable with arrays; as [] is always inferred to never[], which is probably the only type in the system where I am pretty sure it is in 100% of the cases not the type I am looking for :wink:

A similar case is where a lookup map (object) is returned, but initialized as const res = {}, and items are added later on the fly, and the return type is something like { [key:string]: Type }.

ghost commented 7 years ago

That does seem like a bug; I would expect that with implicit any enabled, [] would always be any[], and not never[] even if --strictNullChecks is on.

ahejlsberg commented 7 years ago

With --noImplicitAny enabled we use control flow analysis to determine the type of arrays with no type annotation. See #11432. This is why you're not seeing errors in your examples with --noImplicitAny. It sounds to me like this has the behavior you want without having to depend on return type annotations.

mweststrate commented 7 years ago

@ahejlsberg, @DanielRosenwasser thanks for the explanation. So, the noImplicitAny options results indeed in sane type inference in most cases (saddly don't have that flag enabled yet in the project I run into it). Never realized that this option actually influences type inferences, always thought it was just a flag that forces all things to be typed. So this option solves the original issue in many cases, so feel free to close this issue.

The inference without noImplicitAny of [] to never[] instead of any[] still puzzles me though. Is this for some good reason correct, or a bug indeed?

mweststrate commented 7 years ago

Closing the issue as the original question is answered. (The inferences of [] to never[] is imho a bug but not the original issue). Thanks for the answers!