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

Question: Should let array = [] really mean we want a never[] instead of any[] or is it just my compiler options? #18687

Closed
SMotaal opened this issue Sep 22, 2017 · 11 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@SMotaal
Copy link

SMotaal commented Sep 22, 2017

TypeScript Version: 2.5.2

Code

let array = [];
array.push(1); // tsc error: 1 is not never;
console.log(array);

Compiler Options

{
  "target": "es2017",
  "module": "commonjs",
  "strict": true,
  "noImplicitAny": false,
  "noImplicitThis": false,
  "moduleResolution": "node",
  "experimentalDecorators": true,
  "emitDecoratorMetadata": true
}

Expected behavior

» tsc⏎

# a pause of nothingness

Actual behavior

» tsc

2 array.push(1); // tsc error: {} is not never;
             ~

example.ts(2,12): error TS2345: Argument of type '1' is not assignable to parameter of type 'never'.

Potential causes

After some testing, I did pinpoint the culprit behind this problem in my compiler options:

  "noImplicitAny": false

It seems that setting noImplicitAny to true does not suffer from this issue but for some reason false seems to result in the opposite effect of what is expected.

So in my opinion, somewhere in related to parsing this particular compiler option there might be some assumption related to the various possible values for the tri-states of noImplicitAny.

Sample Project:

typescript-empty-array-issue.zip

» yarn && yarn build

Thanks!

@SMotaal SMotaal changed the title Question: Should let array = [] really mean we want a never[] instead of any[] or is it just me? Question: Should let array = [] really mean we want a never[] instead of any[] or is it just my compiler options? Sep 22, 2017
@RyanCavanaugh
Copy link
Member

This is caused by the combination of strict and noImplicitAny: false. In general we expect that if strict is on, noImplicitAny is also on; this particular set of settings will expose some odd behavior. If you had both on, you'd see an error about the [] being implicitly any[]; if both were off; we'd use control flow analysis and treat the array as a number[] after the push(1);.

The particular combination of settings means that we don't allow ourselves to use control flow analysis or allow the array to be implicitly any[], so never[] is the only remaining allowable option.

I'd recommend turning off strict if you're not going to have noImplicitAny on.

@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Sep 22, 2017
@SMotaal
Copy link
Author

SMotaal commented Sep 25, 2017

I got a chance to try the various combinations and it is consistent. Is there a way to make such combinations — ones that would have predictable but maybe not intuitive side effects from a user's perspective — standout or be flagged to make them realize that.

I am going with an assumption that array defaulting to never is probably not a desired type-checking configuration for most users.

Maybe if the type-checker description for something is not never may trigger reference the options and indicate the face that those two options together are a potential mistake.

Thanks :)

@SMotaal
Copy link
Author

SMotaal commented Sep 29, 2017

@RyanCavanaugh would it be practical to point out in some diagnostic output that those two options together have such implications?

@SMotaal SMotaal closed this as completed Sep 29, 2017
@RyanCavanaugh
Copy link
Member

I'm not sure that's a long-term tractable solution. There are about 80 flags so even conservatively that's 80 * 79 = 6,300 flag combinations which are potential traps to think about. As far as I know you're the first person to notice this, so on balance it's probably not worth handling. We can reconsider if more people find odd combinations of flags that have bad results.

@SMotaal
Copy link
Author

SMotaal commented Oct 3, 2017

I just really hope there aren't actually 6,320 traps out there 👍

@RyanCavanaugh
Copy link
Member

Me too 😅

@bcherny
Copy link

bcherny commented Feb 11, 2018

To follow up on this issue, with TSC 2.7.1 (strict and noImplicitAny enabled) the behavior is not what I would expect:

let a = []         // any[]
a.push(1)          // string[]
a.push('red')      // (number | string)[]

a is implicitly an any[] - why doesn't this throw? I assume this is by design, is there a good discussion somewhere?

Playground link

@RyanCavanaugh
Copy link
Member

@bcherny it's not any[], it's dynamically typed according to what you do with it. If you actually try to induce an unsound any observation, you'll see an error.

@bcherny
Copy link

bcherny commented Feb 12, 2018

@RyanCavanaugh Right. I guess to rephrase, it looks like I can keep widening a's type ad nauseum after I initially constrain it by pushing 1. Ie.

let a = []         // any[]
a.push(1)          // number[]
a.push('red')      // (number | string)[]
a.push(true)       // (number | string | boolean)[]
a.push(a)          // (number | string | boolean | (number | string | boolean)[])[]

I still get safety when reading a member of a, because the union is inferred correctly. It's just surprising that I can keep widening the type. Why did you choose to design it that way, instead of erroring on let a = []? Is it just such a common JS pattern that it would be too verbose to have to explicitly type a upfront?

@RyanCavanaugh
Copy link
Member

It's an improvement on the existing behavior because it gives fewer errors but catches the same type mismatches.

Intuitively, this code

let a = []
a.push(1)
a.push('red')  

should not behave any differently from

let a = [1, 'red']

since we know they're equivalent.

@bcherny
Copy link

bcherny commented Feb 12, 2018

I see. So when a is contextually typed, pushing some type T is semantically similar to initializing with it, so TSC treats them the same. And if the push happens somewhere else (where a is no longer contextually typed), then you have to make explicit what a's type is.

That makes a lot of sense, thanks @RyanCavanaugh!

@microsoft microsoft locked and limited conversation to collaborators Jul 3, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
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

3 participants