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

Disallow more flavors of property accesses on never in expression space #56780

Conversation

Andarist
Copy link
Contributor

fixes #42999
fixes #56778

@typescript-bot typescript-bot added the For Backlog Bug PRs that fix a backlog bug label Dec 14, 2023
let b: 0 | 1 | 9;
[{ [(a = 1)]: b } = [9, a] as const] = [];
~~~~~~~
!!! error TS2339: Property '1' does not exist on type 'never'.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not entirely sure if the error here is completely desirable but it feels consistent to me with things like:

const obj = { b: '' }
let a: number
;({ a = 10 } = obj) // Property 'a' does not exist on type '{ b: string; }'.(2339)

What I mean is that despite the first element in this array pattern having an initializer it's the "right-side" type of the original source that is used to check for valid property accesses.

So since we have never[] on the right side, its first element is actually never and that has no properties so disallowing those patterns here looks desirable to me.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This error doesn't look right to me: it says "Property '1' does not exist on type 'never'", but we should be getting property 1 of type [9, typeof a], and that tuple should not have type never.

@gabritto

This comment was marked as duplicate.

@gabritto
Copy link
Member

gabritto commented Jan 2, 2024

@typescript-bot pack this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Jan 2, 2024

Heya @gabritto, I've started to run the tarball bundle task on this PR at ba7033b. You can monitor the build here.

@gabritto
Copy link
Member

gabritto commented Jan 2, 2024

@typescript-bot run DT
@typescript-bot user test this
@typescript-bot test top100
@typescript-bot perf test this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Jan 2, 2024

Heya @gabritto, I've started to run the parallelized Definitely Typed test suite on this PR at ba7033b. You can monitor the build here.

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Jan 2, 2024

Heya @gabritto, I've started to run the diff-based user code test suite on this PR at ba7033b. You can monitor the build here.

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Jan 2, 2024

Heya @gabritto, I've started to run the regular perf test suite on this PR at ba7033b. You can monitor the build here.

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Jan 2, 2024

Heya @gabritto, I've started to run the diff-based top-repos suite on this PR at ba7033b. You can monitor the build here.

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Jan 2, 2024

Hey @gabritto, I've packed this into an installable tgz. You can install it for testing by referencing it in your package.json like so:

{
    "devDependencies": {
        "typescript": "https://typescript.visualstudio.com/cf7ac146-d525-443c-b23c-0d58337efebc/_apis/build/builds/159198/artifacts?artifactName=tgz&fileId=DEA186CBADFCFA1EFA59EE24FB2DE9C56FC114FDD17906CCB70D934F7EDDCE5F02&fileName=/typescript-5.4.0-insiders.20240102.tgz"
    }
}

and then running npm install.


There is also a playground for this build and an npm module you can use via "typescript": "npm:@typescript-deploys/pr-build@5.4.0-pr-56780-8".;

@typescript-bot
Copy link
Collaborator

@gabritto Here are the results of running the user test suite comparing main and refs/pull/56780/merge:

There were infrastructure failures potentially unrelated to your change:

  • 1 instance of "Package install failed"

Otherwise...

Something interesting changed - please have a look.

Details

enhanced-resolve

/mnt/ts_downloads/enhanced-resolve/tsconfig.json

  • [MISSING] error TS2339: Property 'replace' does not exist on type 'never'.
    • /mnt/ts_downloads/enhanced-resolve/node_modules/enhanced-resolve/lib/concord.js(149,24)
    • /mnt/ts_downloads/enhanced-resolve/node_modules/enhanced-resolve/lib/concord.js(188,17)

puppeteer

packages/browsers/test/src/tsconfig.json

@typescript-bot
Copy link
Collaborator

@gabritto
The results of the perf run you requested are in!

Here they are:

Compiler

Comparison Report - baseline..pr
Metric baseline pr Delta Best Worst p-value
Angular - node (v18.15.0, x64)
Memory used 295,447k (± 0.01%) 295,447k (± 0.01%) ~ 295,416k 295,491k p=1.000 n=6
Parse Time 2.65s (± 0.44%) 2.65s (± 0.28%) ~ 2.64s 2.66s p=0.933 n=6
Bind Time 0.82s (± 0.50%) 0.82s (± 1.20%) ~ 0.81s 0.84s p=0.753 n=6
Check Time 8.14s (± 0.34%) 8.16s (± 0.30%) ~ 8.14s 8.20s p=0.145 n=6
Emit Time 7.12s (± 0.24%) 7.10s (± 0.27%) ~ 7.08s 7.13s p=0.103 n=6
Total Time 18.73s (± 0.17%) 18.74s (± 0.20%) ~ 18.71s 18.81s p=0.871 n=6
Compiler-Unions - node (v18.15.0, x64)
Memory used 191,532k (± 0.02%) 192,473k (± 1.24%) ~ 191,419k 197,330k p=0.810 n=6
Parse Time 1.35s (± 0.56%) 1.35s (± 0.56%) ~ 1.34s 1.36s p=1.000 n=6
Bind Time 0.72s (± 0.00%) 0.72s (± 0.00%) ~ 0.72s 0.72s p=1.000 n=6
Check Time 9.29s (± 0.61%) 9.27s (± 0.34%) ~ 9.23s 9.30s p=0.370 n=6
Emit Time 2.63s (± 0.79%) 2.61s (± 0.56%) ~ 2.59s 2.63s p=0.167 n=6
Total Time 13.98s (± 0.39%) 13.94s (± 0.20%) ~ 13.90s 13.97s p=0.294 n=6
Monaco - node (v18.15.0, x64)
Memory used 347,396k (± 0.00%) 347,394k (± 0.01%) ~ 347,338k 347,431k p=0.810 n=6
Parse Time 2.47s (± 0.36%) 2.46s (± 0.63%) ~ 2.45s 2.49s p=0.117 n=6
Bind Time 0.93s (± 0.44%) 0.93s (± 1.18%) ~ 0.92s 0.95s p=1.000 n=6
Check Time 6.88s (± 0.42%) 6.88s (± 0.30%) ~ 6.85s 6.90s p=0.935 n=6
Emit Time 4.04s (± 0.41%) 4.05s (± 0.57%) ~ 4.03s 4.09s p=1.000 n=6
Total Time 14.31s (± 0.16%) 14.31s (± 0.36%) ~ 14.26s 14.38s p=0.935 n=6
TFS - node (v18.15.0, x64)
Memory used 302,716k (± 0.01%) 302,732k (± 0.00%) ~ 302,719k 302,747k p=0.128 n=6
Parse Time 1.99s (± 0.61%) 2.00s (± 0.97%) ~ 1.97s 2.02s p=0.327 n=6
Bind Time 1.00s (± 0.98%) 1.00s (± 0.55%) ~ 0.99s 1.00s p=0.201 n=6
Check Time 6.29s (± 0.59%) 6.31s (± 0.19%) ~ 6.29s 6.32s p=0.625 n=6
Emit Time 3.58s (± 0.37%) 3.59s (± 0.42%) ~ 3.56s 3.60s p=0.284 n=6
Total Time 12.86s (± 0.28%) 12.89s (± 0.17%) ~ 12.87s 12.93s p=0.413 n=6
material-ui - node (v18.15.0, x64)
Memory used 506,818k (± 0.00%) 506,837k (± 0.00%) ~ 506,814k 506,852k p=0.173 n=6
Parse Time 2.59s (± 0.47%) 2.58s (± 0.62%) ~ 2.57s 2.61s p=0.209 n=6
Bind Time 0.99s (± 0.52%) 0.99s (± 0.64%) ~ 0.98s 1.00s p=0.386 n=6
Check Time 16.93s (± 0.26%) 16.90s (± 0.44%) ~ 16.80s 16.98s p=0.520 n=6
Emit Time 0.00s (± 0.00%) 0.00s (± 0.00%) ~ 0.00s 0.00s p=1.000 n=6
Total Time 20.50s (± 0.21%) 20.47s (± 0.36%) ~ 20.37s 20.55s p=0.518 n=6
xstate - node (v18.15.0, x64)
Memory used 512,893k (± 0.01%) 512,930k (± 0.01%) ~ 512,894k 512,985k p=0.173 n=6
Parse Time 3.27s (± 0.23%) 3.28s (± 0.42%) ~ 3.25s 3.29s p=0.196 n=6
Bind Time 1.53s (± 0.36%) 1.54s (± 0.34%) ~ 1.53s 1.54s p=0.640 n=6
Check Time 2.81s (± 0.37%) 2.84s (± 1.05%) ~ 2.81s 2.88s p=0.096 n=6
Emit Time 0.07s (± 0.00%) 0.07s (± 7.03%) ~ 0.07s 0.08s p=0.174 n=6
Total Time 7.70s (± 0.13%) 7.73s (± 0.29%) +0.03s (+ 0.39%) 7.70s 7.75s p=0.019 n=6
System info unknown
Hosts
  • node (v18.15.0, x64)
Scenarios
  • Angular - node (v18.15.0, x64)
  • Compiler-Unions - node (v18.15.0, x64)
  • Monaco - node (v18.15.0, x64)
  • TFS - node (v18.15.0, x64)
  • material-ui - node (v18.15.0, x64)
  • xstate - node (v18.15.0, x64)
Benchmark Name Iterations
Current pr 6
Baseline baseline 6

tsserver

Comparison Report - baseline..pr
Metric baseline pr Delta Best Worst p-value
Compiler-UnionsTSServer - node (v18.15.0, x64)
Req 1 - updateOpen 2,343ms (± 0.82%) 2,356ms (± 0.56%) ~ 2,331ms 2,368ms p=0.297 n=6
Req 2 - geterr 5,447ms (± 1.41%) 5,411ms (± 1.22%) ~ 5,353ms 5,538ms p=0.378 n=6
Req 3 - references 326ms (± 1.39%) 325ms (± 1.18%) ~ 321ms 331ms p=0.687 n=6
Req 4 - navto 277ms (± 1.22%) 279ms (± 0.98%) ~ 273ms 280ms p=0.511 n=6
Req 5 - completionInfo count 1,356 (± 0.00%) 1,356 (± 0.00%) ~ 1,356 1,356 p=1.000 n=6
Req 5 - completionInfo 89ms (± 5.95%) 84ms (± 4.70%) ~ 82ms 92ms p=0.082 n=6
CompilerTSServer - node (v18.15.0, x64)
Req 1 - updateOpen 2,494ms (± 1.27%) 2,458ms (± 1.29%) ~ 2,404ms 2,484ms p=0.128 n=6
Req 2 - geterr 4,187ms (± 1.84%) 4,159ms (± 1.89%) ~ 4,079ms 4,232ms p=0.575 n=6
Req 3 - references 338ms (± 1.27%) 341ms (± 1.58%) ~ 335ms 347ms p=0.288 n=6
Req 4 - navto 288ms (± 1.24%) 286ms (± 1.32%) ~ 282ms 291ms p=0.370 n=6
Req 5 - completionInfo count 1,518 (± 0.00%) 1,518 (± 0.00%) ~ 1,518 1,518 p=1.000 n=6
Req 5 - completionInfo 82ms (± 7.14%) 84ms (± 7.81%) ~ 76ms 90ms p=0.805 n=6
xstateTSServer - node (v18.15.0, x64)
Req 1 - updateOpen 2,605ms (± 0.36%) 2,598ms (± 0.57%) ~ 2,580ms 2,618ms p=0.471 n=6
Req 2 - geterr 1,708ms (± 1.75%) 1,729ms (± 2.12%) ~ 1,678ms 1,774ms p=0.378 n=6
Req 3 - references 109ms (± 9.49%) 112ms (± 9.97%) ~ 101ms 123ms p=0.931 n=6
Req 4 - navto 364ms (± 0.27%) 365ms (± 0.21%) +1ms (+ 0.37%) 364ms 366ms p=0.027 n=6
Req 5 - completionInfo count 2,073 (± 0.00%) 2,073 (± 0.00%) ~ 2,073 2,073 p=1.000 n=6
Req 5 - completionInfo 310ms (± 1.64%) 308ms (± 1.69%) ~ 303ms 314ms p=0.685 n=6
System info unknown
Hosts
  • node (v18.15.0, x64)
Scenarios
  • CompilerTSServer - node (v18.15.0, x64)
  • Compiler-UnionsTSServer - node (v18.15.0, x64)
  • xstateTSServer - node (v18.15.0, x64)
Benchmark Name Iterations
Current pr 6
Baseline baseline 6

Startup

Comparison Report - baseline..pr
Metric baseline pr Delta Best Worst p-value
tsc-startup - node (v18.15.0, x64)
Execution time 153.36ms (± 0.19%) 153.38ms (± 0.20%) ~ 152.31ms 158.36ms p=0.416 n=600
tsserver-startup - node (v18.15.0, x64)
Execution time 228.48ms (± 0.15%) 228.21ms (± 0.14%) -0.28ms (- 0.12%) 226.79ms 231.35ms p=0.000 n=600
tsserverlibrary-startup - node (v18.15.0, x64)
Execution time 230.01ms (± 0.19%) 230.04ms (± 0.19%) ~ 228.22ms 236.76ms p=0.310 n=600
typescript-startup - node (v18.15.0, x64)
Execution time 230.45ms (± 0.19%) 230.53ms (± 0.18%) ~ 228.80ms 233.52ms p=0.058 n=600
System info unknown
Hosts
  • node (v18.15.0, x64)
Scenarios
  • tsc-startup - node (v18.15.0, x64)
  • tsserver-startup - node (v18.15.0, x64)
  • tsserverlibrary-startup - node (v18.15.0, x64)
  • typescript-startup - node (v18.15.0, x64)
Benchmark Name Iterations
Current pr 6
Baseline baseline 6

Developer Information:

Download Benchmarks

@typescript-bot
Copy link
Collaborator

Hey @gabritto, the results of running the DT tests are ready.
Everything looks the same!
You can check the log here.

@typescript-bot
Copy link
Collaborator

@gabritto Here are the results of running the top-repos suite comparing main and refs/pull/56780/merge:

Something interesting changed - please have a look.

Details

ant-design/ant-design

1 of 2 projects failed to build with the old tsc and were ignored

tsconfig.json

backstage/backstage

6 of 13 projects failed to build with the old tsc and were ignored

packages/cli/templates/default-react-plugin-package/tsconfig.json

microsoft/vscode

3 of 54 projects failed to build with the old tsc and were ignored

src/tsconfig.json

src/tsconfig.tsec.json

tldraw/tldraw

1 of 5 projects failed to build with the old tsc and were ignored

apps/examples/tsconfig.json

apps/vscode/editor/tsconfig.json

apps/vscode/extension/tsconfig.json

@gabritto
Copy link
Member

gabritto commented Jan 2, 2024

@gabritto Here are the results of running the top-repos suite comparing main and refs/pull/56780/merge:

Something interesting changed - please have a look.

Details

ant-design/ant-design

backstage/backstage

microsoft/vscode

tldraw/tldraw

Of the new errors on the top repos test, most seem to be because of "defensive programming", where there is an exhaustive check on some variable followed by a branch for when it has an unexpected type. In this defensive branch, the variable has type never, so an element access produces an error.

Those seem to be actual, desirable errors:
https://github.com/ant-design/ant-design/blob/4704eb578b8e7caabc502a5204c2ba058ee8266c/components/tree/utils/dictUtil.ts#L22
https://github.com/ant-design/ant-design/blob/4704eb578b8e7caabc502a5204c2ba058ee8266c/components/tree/utils/dictUtil.ts#L22

@gabritto
Copy link
Member

gabritto commented Jan 3, 2024

We discussed this at design meeting today, and the conclusion was that this change is not worth the breaks it causes. As evidenced by the extended tests, most new errors are not really desirable and come from an intentional usage of never, i.e. something has type never in code that is not supposed to be reachable.
There was one error that seemed like a bug, as I pointed out above, where the code looks like function foo(x: A & B) { ... x[key] ... }, and A & B reduces to never.
So it seems like most times, people are using never intentionally and TS shouldn't error, but other times it should, and it's not immediately clear how to tell the difference.

@jakebailey
Copy link
Member

I unfortunately missed the meeting, but I find it a little weird that we're saying that it's okay to access any prop on never; if that were the case, then Foo | never should not reduce because a component of the union "has all props", but that's not what never does... It's the empty set of types, with which you can do nothing.

I personally feel like all of the errors this PR showed are real and that never is a type that's okay to have around, and that if you want to touch something that's never you need to cast it to something else to probe at it.

@gabritto
Copy link
Member

gabritto commented Jan 5, 2024

It's the empty set of types, with which you can do nothing.

Since we know never is the empty set of values, once you know you have a value v of type never, you've reached a contradiction. And, by the principle of "from contradiction, anything follows", you can access anything on that value because anything is true about it, including statements like "v has property blah".
If we had a sound type system, at run time we'd never reach that portion of code where v has type never, so it would not matter if you tried to access a property on it.

I unfortunately missed the meeting, but I find it a little weird that we're saying that it's okay to access any prop on never; if that were the case, then Foo | never should not reduce because a component of the union "has all props", but that's not what never does...

Foo | never reduces to Foo because you know whatever value has that type, it can't have type never.
But let's say we didn't reduce it and you have a value v of type Foo | never. v either has type never, in which case you can access any property on v, or it has type Foo, in which case you can only access Foo properties on v. In both of those cases, you can access the Foo properties on v. This is consistent with saying v has type Foo.

@jakebailey
Copy link
Member

Foo | never reduces to Foo because you know whatever value has that type, it can't have type never.
But let's say we didn't reduce it and you have a value v of type Foo | never. v either has type never, in which case you can access any property on v, or it has type Foo, in which case you can only access Foo properties on v. In both of those cases, you can access the Foo properties on v. This is consistent with saying v has type Foo.

Ah, that is true; I had it flipped in my mind; obviously if you have a union of two types, you can only access what they have in common.

@Andarist
Copy link
Contributor Author

Andarist commented Jan 5, 2024

I'm OK with the judgment itself but this inconsistency is killing me:

declare const foo: never

foo.prop // error ?????
foo["prop"] // ok
const { prop } = foo // ok

It feels just wildly inconsistent and hard to explain to people. How this difference in behavior would be explained in the docs?

I didn't yet have the time to review carefully everything reported by the extended test suite but I'd definitely call most of those failures improvements. I feel like the only use case that was said to be valid here is the defensive programming case.

And to quote @jakebailey:

if you want to touch something that's never you need to cast it to something else to probe at it.

It would at least give some visual indicator that something funky is going on there. Those would also often appear in the codepaths that would be clearly associated with such defensive programming fallbacks (close to thrown error messages, console.warns etc). I feel like in those contexts people wouldn't mind at all the cast.

On the other hand, I'm quite worried - as the user - that never might accidentally sneak past me like this and I won't notice it at all. For instance, I doubt that this intentionally relies on never this way.

This isn't exactly a new risk though. never is assignable to everything so it's fairly easy (in the same way) to accidentally pass it to a function as an argument. That leads me to be OK with the final judgment. That inconsistency though... if this is the final judgment, shouldn't foo.prop be relaxed to allow this access on never?

@gabritto
Copy link
Member

gabritto commented Jan 6, 2024

I'm OK with the judgment itself but this inconsistency is killing me:

declare const foo: never

foo.prop // error ?????
foo["prop"] // ok
const { prop } = foo // ok

It feels just wildly inconsistent and hard to explain to people. How this difference in behavior would be explained in the docs?

I'm not even sure we did that on purpose, I'll have to investigate.

I feel like in those contexts people wouldn't mind at all the cast.

I'm open to believing that, given evidence, but it could still be a burden to make this update if it's needed in too many places.

On the other hand, I'm quite worried - as the user - that never might accidentally sneak past me like this and I won't notice it at all. For instance, I doubt that this intentionally relies on never this way.

Yeah, that was a genuine error, as I pointed out above.

This isn't exactly a new risk though. never is assignable to everything so it's fairly easy (in the same way) to accidentally pass it to a function as an argument. That leads me to be OK with the final judgment. That inconsistency though... if this is the final judgment, shouldn't foo.prop be relaxed to allow this access on never?

I'm having this thought that never is kind of like an any in the sense that you can do almost anything with it, so I agree that it's risky.
I think this isn't necessarily a "final judgement" in that we'll never ever do it, but we aren't currently convinced this is worth the break.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
For Backlog Bug PRs that fix a backlog bug
Projects
Archived in project
5 participants