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

Created on 22 Sep 2017  ·  11Comments  ·  Source: microsoft/TypeScript



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!

Working as Intended

Most helpful comment

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.

All 11 comments

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.

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 :)

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

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.

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

Me too 😅

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

@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.

@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?

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.

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!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

kyasbal-1994 picture kyasbal-1994  ·  3Comments

dlaberge picture dlaberge  ·  3Comments

seanzer picture seanzer  ·  3Comments

zhuravlikjb picture zhuravlikjb  ·  3Comments

wmaurer picture wmaurer  ·  3Comments