Skip to content

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

@SMotaal

Description

@SMotaal

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!

Activity

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?[/+] on Sep 22, 2017
RyanCavanaugh

RyanCavanaugh commented on Sep 22, 2017

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

SMotaal

SMotaal commented on Sep 25, 2017

@SMotaal
Author

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

SMotaal commented on Sep 29, 2017

@SMotaal
Author

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

RyanCavanaugh

RyanCavanaugh commented on Oct 3, 2017

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

SMotaal commented on Oct 3, 2017

@SMotaal
Author

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

RyanCavanaugh

RyanCavanaugh commented on Oct 3, 2017

@RyanCavanaugh
Member

Me too 😅

bcherny

bcherny commented on Feb 11, 2018

@bcherny

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

RyanCavanaugh commented on Feb 12, 2018

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

bcherny commented on Feb 12, 2018

@bcherny

@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

RyanCavanaugh commented on Feb 12, 2018

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

bcherny commented on Feb 12, 2018

@bcherny

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!

locked and limited conversation to collaborators on Jul 3, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Working as IntendedThe behavior described is the intended behavior; this is not a bug

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @SMotaal@bcherny@RyanCavanaugh

        Issue actions

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