microsoft / TypeScript

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

Type inference only works on the same line as the variable declaration #41289

Open akatechis opened 3 years ago

akatechis commented 3 years ago

I ran into this odd behavior while writing some tests, that do the typical describe, declare a closure-scope variable, and in beforeEach, assign some object to that variable that is used in each of the tests inside.

Search Terms: infer, inference, closure, scope, broken, any, cast

Initially, I thought it was limited to closure scope, but I then started reducing the reproduction sample more and more, and realized, it's broken even if it's on the very next line:

Code

// given
interface Foo {
    id: string;
}

function make(): Foo {
    return {
        id: 'akatechis',
    }
}

// u1 is correctly inferred to be of type Foo.
let u1 = make();

// u2 is incorrectly inferred to be of type any.
let u2;
u2 = make();

// this was the scenario in which I originally ran into this:
// a closure scoped variable that is assigned in an inner function
function outer () {
    // u3 is inferred to be of type any
    let u3;
    return {
        inner() {
            // u3 here is still any
            u3 = make();

            // surprisingly, returning u3 here causes the return type of inner() to be Foo, not any???
            return u3;
        }
    }
}

Expected type declarations:

interface Foo {
    id: string;
}
declare function make(): Foo;
declare let u1: Foo;
declare let u2: Foo; // <---- this should have been inferred to be Foo
declare function outer(): {
    inner(): Foo;
};

Actual type declarations:

interface Foo {
    id: string;
}
declare function make(): Foo;
declare let u1: Foo;
declare let u2: any;
declare function outer(): {
    inner(): Foo;
};

Playground Link: playground

Related Issues: The only thing that I found somewhat close to this issue was the question in the FAQ about unused generic types causing type widening, but I don't think this is it, because I'm not using generics here.

DanielRosenwasser commented 3 years ago

Under noImplicitAny we can auto-type types via control flow analysis. In assignments, it appears as any because it does permit assignments of any type to it.

let u2;
u2 = make();
u2; // request quick info here

You're right that u2 gets a .d.ts emit of type any - that's probably undesirable under noImplicitAny; however, you're also requesting .d.ts emit on a global script file. If you switch u2 to an export, you will get a noImplicitAny error.

DanielRosenwasser commented 3 years ago

If you have .d.ts emit on, it might make sense to either provide an implicit any warning.

Alternatively, we could emit the type as unknown; but the latter seems unfortunate for type consumers.

akatechis commented 3 years ago

In my first encounter with this, I was writing some mocha tests, that I run with ts-node. My tsconfig file does indeed have noImplicitAny, and I was getting TypeErrors on the relevant line.

That said, I believe the deeper issue is that it's infering it to be of type any in the first place, not whether TS is emiting a warning or error. Is there a reason we would ever want this to be inferred as any, especially when the same script/module contains an assignment that would hint at the type of u2?

let u2;
u2 = make();

I'm not familiar with the internals of TSC and all the parts of the TS language itself that might conflict with what seems like a simple thing that would improve the developer experience, so idk if this is easy or hard.