Skip to content

Unify logic in typeof narrowing #33434

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

Merged
merged 2 commits into from
May 6, 2020

Conversation

jack-williams
Copy link
Collaborator

@jack-williams jack-williams commented Sep 14, 2019

In looking to resolve #33180 I want something like if (typeof x === "boolean") { ... } to narrow x to boolean when x is of type boolean | void, such behaviour is consistent with empty intersection reduction.

Doing so requires changing both if and switch logic. It also feels more robust moving forward to unify the logic in if (typeof x === "boolean") { ... } and switch (typeof x) { ... }, so I'm looking to merge this first and then fix the void case---hoping to avoid conflating any regressions.

@jack-williams
Copy link
Collaborator Author

@typescript-bot test this
@typescript-bot user test this
@typescript-bot perf test this

because bots don't get weekends.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Sep 14, 2019

Heya @jack-williams, I've started to run the extended test suite on this PR at 8be155d. You can monitor the build here. It should now contribute to this PR's status checks.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Sep 14, 2019

Heya @jack-williams, I've started to run the perf test suite on this PR at 8be155d. You can monitor the build here. It should now contribute to this PR's status checks.

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Sep 14, 2019

Heya @jack-williams, I've started to run the parallelized community code test suite on this PR at 8be155d. You can monitor the build here. It should now contribute to this PR's status checks.

@typescript-bot
Copy link
Collaborator

@jack-williams
The results of the perf run you requested are in!

Here they are:

Comparison Report - master..33434

Metric master 33434 Delta Best Worst
Angular - node (v12.1.0, x64)
Memory used 331,205k (± 0.06%) 331,611k (± 0.02%) +406k (+ 0.12%) 331,431k 331,787k
Parse Time 1.56s (± 0.44%) 1.56s (± 0.48%) +0.00s (+ 0.26%) 1.54s 1.57s
Bind Time 0.78s (± 0.00%) 0.78s (± 0.61%) +0.00s (+ 0.38%) 0.78s 0.80s
Check Time 4.26s (± 0.58%) 4.29s (± 0.47%) +0.02s (+ 0.59%) 4.24s 4.34s
Emit Time 5.25s (± 1.18%) 5.27s (± 0.96%) +0.02s (+ 0.34%) 5.17s 5.40s
Total Time 11.85s (± 0.68%) 11.90s (± 0.37%) +0.05s (+ 0.41%) 11.81s 11.99s
Monaco - node (v12.1.0, x64)
Memory used 345,970k (± 0.01%) 345,931k (± 0.02%) -39k (- 0.01%) 345,827k 346,092k
Parse Time 1.23s (± 0.54%) 1.22s (± 0.85%) -0.01s (- 0.41%) 1.20s 1.24s
Bind Time 0.67s (± 0.71%) 0.67s (± 0.92%) -0.00s (- 0.30%) 0.65s 0.68s
Check Time 4.25s (± 0.39%) 4.24s (± 0.29%) -0.00s (- 0.05%) 4.22s 4.27s
Emit Time 2.83s (± 0.68%) 2.85s (± 0.98%) +0.02s (+ 0.81%) 2.80s 2.93s
Total Time 8.97s (± 0.21%) 8.98s (± 0.31%) +0.01s (+ 0.17%) 8.94s 9.06s
TFS - node (v12.1.0, x64)
Memory used 301,424k (± 0.01%) 301,421k (± 0.01%) -4k (- 0.00%) 301,353k 301,514k
Parse Time 0.95s (± 0.58%) 0.95s (± 0.74%) -0.00s (- 0.11%) 0.94s 0.97s
Bind Time 0.62s (± 1.21%) 0.62s (± 1.07%) -0.00s (- 0.16%) 0.61s 0.64s
Check Time 3.85s (± 0.40%) 3.86s (± 0.29%) +0.02s (+ 0.47%) 3.84s 3.89s
Emit Time 2.99s (± 1.53%) 2.96s (± 0.97%) -0.02s (- 0.80%) 2.91s 3.04s
Total Time 8.40s (± 0.64%) 8.40s (± 0.43%) -0.00s (- 0.06%) 8.33s 8.48s
Angular - node (v8.9.0, x64)
Memory used 350,106k (± 0.02%) 350,466k (± 0.01%) +360k (+ 0.10%) 350,353k 350,589k
Parse Time 2.09s (± 0.49%) 2.11s (± 0.25%) +0.02s (+ 0.86%) 2.10s 2.12s
Bind Time 0.83s (± 0.78%) 0.84s (± 0.95%) +0.01s (+ 0.72%) 0.82s 0.86s
Check Time 5.11s (± 0.59%) 5.13s (± 0.58%) +0.01s (+ 0.27%) 5.05s 5.20s
Emit Time 5.99s (± 1.00%) 5.98s (± 1.13%) -0.01s (- 0.25%) 5.86s 6.14s
Total Time 14.03s (± 0.45%) 14.05s (± 0.58%) +0.02s (+ 0.17%) 13.87s 14.19s
Monaco - node (v8.9.0, x64)
Memory used 363,734k (± 0.02%) 363,752k (± 0.01%) +18k (+ 0.00%) 363,695k 363,841k
Parse Time 1.56s (± 0.57%) 1.56s (± 0.30%) -0.00s (- 0.06%) 1.55s 1.57s
Bind Time 0.89s (± 0.63%) 0.89s (± 0.75%) +0.00s (+ 0.56%) 0.88s 0.91s
Check Time 5.08s (± 1.43%) 5.08s (± 1.46%) +0.00s (+ 0.08%) 5.01s 5.37s
Emit Time 3.20s (± 4.18%) 3.29s (± 2.27%) +0.09s (+ 2.69%) 2.99s 3.34s
Total Time 10.73s (± 0.72%) 10.82s (± 0.26%) +0.10s (+ 0.90%) 10.76s 10.88s
TFS - node (v8.9.0, x64)
Memory used 317,699k (± 0.01%) 317,716k (± 0.02%) +17k (+ 0.01%) 317,570k 317,800k
Parse Time 1.26s (± 0.79%) 1.26s (± 0.44%) -0.00s (- 0.16%) 1.25s 1.27s
Bind Time 0.68s (± 5.26%) 0.69s (± 4.95%) +0.00s (+ 0.15%) 0.65s 0.78s
Check Time 4.45s (± 1.03%) 4.43s (± 1.36%) -0.02s (- 0.45%) 4.28s 4.51s
Emit Time 3.08s (± 0.48%) 3.06s (± 0.67%) -0.02s (- 0.75%) 3.00s 3.11s
Total Time 9.48s (± 0.35%) 9.44s (± 0.41%) -0.04s (- 0.45%) 9.38s 9.56s
Angular - node (v8.9.0, x86)
Memory used 198,198k (± 0.02%) 198,391k (± 0.03%) +192k (+ 0.10%) 198,247k 198,508k
Parse Time 2.03s (± 0.66%) 2.04s (± 0.73%) +0.01s (+ 0.64%) 2.01s 2.08s
Bind Time 0.95s (± 0.36%) 0.95s (± 0.82%) +0.00s (+ 0.42%) 0.94s 0.97s
Check Time 4.65s (± 0.49%) 4.67s (± 0.50%) +0.02s (+ 0.37%) 4.61s 4.73s
Emit Time 5.64s (± 0.61%) 5.66s (± 0.78%) +0.02s (+ 0.32%) 5.59s 5.82s
Total Time 13.27s (± 0.35%) 13.32s (± 0.39%) +0.05s (+ 0.37%) 13.22s 13.45s
Monaco - node (v8.9.0, x86)
Memory used 203,231k (± 0.01%) 203,240k (± 0.01%) +9k (+ 0.00%) 203,203k 203,322k
Parse Time 1.62s (± 0.85%) 1.61s (± 0.47%) -0.01s (- 0.62%) 1.59s 1.63s
Bind Time 0.72s (± 0.82%) 0.72s (± 0.72%) -0.00s (- 0.41%) 0.71s 0.73s
Check Time 4.86s (± 0.38%) 4.88s (± 0.40%) +0.02s (+ 0.41%) 4.82s 4.91s
Emit Time 3.16s (± 0.53%) 3.16s (± 0.41%) -0.00s (- 0.03%) 3.12s 3.19s
Total Time 10.36s (± 0.27%) 10.36s (± 0.17%) +0.00s (+ 0.02%) 10.31s 10.40s
TFS - node (v8.9.0, x86)
Memory used 178,553k (± 0.01%) 178,554k (± 0.01%) +2k (+ 0.00%) 178,509k 178,612k
Parse Time 1.31s (± 0.45%) 1.31s (± 0.31%) -0.00s (- 0.23%) 1.30s 1.32s
Bind Time 0.64s (± 0.81%) 0.64s (± 0.75%) -0.00s (- 0.31%) 0.63s 0.65s
Check Time 4.29s (± 0.61%) 4.29s (± 0.73%) +0.00s (+ 0.07%) 4.20s 4.35s
Emit Time 2.87s (± 1.54%) 2.86s (± 0.90%) -0.01s (- 0.35%) 2.79s 2.91s
Total Time 9.11s (± 0.67%) 9.10s (± 0.53%) -0.01s (- 0.10%) 9.00s 9.20s
Angular - node (v9.0.0, x64)
Memory used 349,750k (± 0.02%) 350,119k (± 0.02%) +369k (+ 0.11%) 350,022k 350,339k
Parse Time 1.82s (± 0.44%) 1.82s (± 0.46%) +0.00s (+ 0.06%) 1.80s 1.83s
Bind Time 0.78s (± 0.64%) 0.78s (± 0.94%) +0.00s (+ 0.39%) 0.76s 0.79s
Check Time 4.87s (± 0.56%) 4.87s (± 0.53%) +0.01s (+ 0.16%) 4.82s 4.94s
Emit Time 5.78s (± 1.22%) 5.74s (± 1.29%) -0.04s (- 0.74%) 5.59s 5.89s
Total Time 13.24s (± 0.68%) 13.21s (± 0.54%) -0.03s (- 0.21%) 13.02s 13.37s
Monaco - node (v9.0.0, x64)
Memory used 363,360k (± 0.01%) 363,521k (± 0.02%) +161k (+ 0.04%) 363,327k 363,643k
Parse Time 1.31s (± 0.44%) 1.32s (± 0.66%) +0.00s (+ 0.38%) 1.30s 1.34s
Bind Time 0.84s (± 0.44%) 0.84s (± 1.01%) -0.01s (- 0.71%) 0.82s 0.85s
Check Time 4.87s (± 0.33%) 4.95s (± 1.77%) +0.08s (+ 1.73%) 4.84s 5.13s
Emit Time 3.36s (± 0.54%) 3.21s (± 5.05%) -0.14s (- 4.26%) 2.87s 3.38s
Total Time 10.38s (± 0.33%) 10.32s (± 0.95%) -0.06s (- 0.56%) 10.12s 10.51s
TFS - node (v9.0.0, x64)
Memory used 317,508k (± 0.01%) 317,446k (± 0.01%) -61k (- 0.02%) 317,365k 317,503k
Parse Time 1.04s (± 0.59%) 1.04s (± 0.73%) +0.00s (+ 0.48%) 1.02s 1.06s
Bind Time 0.62s (± 0.84%) 0.62s (± 0.76%) +0.00s (+ 0.65%) 0.62s 0.64s
Check Time 4.38s (± 0.43%) 4.40s (± 0.41%) +0.02s (+ 0.34%) 4.37s 4.45s
Emit Time 3.17s (± 1.80%) 3.21s (± 1.00%) +0.03s (+ 0.98%) 3.11s 3.25s
Total Time 9.21s (± 0.76%) 9.27s (± 0.46%) +0.06s (+ 0.63%) 9.15s 9.34s
Angular - node (v9.0.0, x86)
Memory used 198,395k (± 0.03%) 198,522k (± 0.02%) +127k (+ 0.06%) 198,429k 198,658k
Parse Time 1.73s (± 0.45%) 1.72s (± 0.54%) -0.01s (- 0.40%) 1.71s 1.75s
Bind Time 0.89s (± 0.73%) 0.90s (± 1.24%) +0.00s (+ 0.45%) 0.87s 0.93s
Check Time 4.34s (± 0.45%) 4.35s (± 0.85%) +0.01s (+ 0.30%) 4.31s 4.47s
Emit Time 5.49s (± 0.59%) 5.55s (± 0.97%) +0.07s (+ 1.20%) 5.46s 5.71s
Total Time 12.44s (± 0.32%) 12.52s (± 0.55%) +0.08s (+ 0.61%) 12.38s 12.68s
Monaco - node (v9.0.0, x86)
Memory used 203,301k (± 0.02%) 203,319k (± 0.02%) +18k (+ 0.01%) 203,236k 203,439k
Parse Time 1.34s (± 0.45%) 1.34s (± 0.51%) 0.00s ( 0.00%) 1.33s 1.36s
Bind Time 0.65s (± 1.03%) 0.64s (± 1.06%) -0.00s (- 0.46%) 0.63s 0.66s
Check Time 4.67s (± 0.63%) 4.69s (± 0.42%) +0.01s (+ 0.24%) 4.65s 4.75s
Emit Time 3.07s (± 0.46%) 3.09s (± 0.65%) +0.02s (+ 0.59%) 3.04s 3.12s
Total Time 9.73s (± 0.26%) 9.76s (± 0.27%) +0.03s (+ 0.28%) 9.70s 9.81s
TFS - node (v9.0.0, x86)
Memory used 178,605k (± 0.01%) 178,604k (± 0.02%) -0k (- 0.00%) 178,539k 178,720k
Parse Time 1.06s (± 0.70%) 1.06s (± 0.56%) +0.00s (+ 0.09%) 1.05s 1.08s
Bind Time 0.57s (± 0.86%) 0.58s (± 0.81%) +0.01s (+ 1.05%) 0.57s 0.59s
Check Time 4.13s (± 0.58%) 4.16s (± 0.30%) +0.03s (+ 0.73%) 4.14s 4.19s
Emit Time 2.80s (± 1.06%) 2.78s (± 1.08%) -0.01s (- 0.46%) 2.72s 2.83s
Total Time 8.56s (± 0.56%) 8.58s (± 0.47%) +0.02s (+ 0.28%) 8.49s 8.64s
System
Machine Namets-ci-ubuntu
Platformlinux 4.4.0-161-generic
Architecturex64
Available Memory16 GB
Available Memory10 GB
CPUs4 × Intel(R) Core(TM) i7-4770 CPU @ 3.40GHz
Hosts
  • node (v12.1.0, x64)
  • node (v8.9.0, x64)
  • node (v8.9.0, x86)
  • node (v9.0.0, x64)
  • node (v9.0.0, x86)
Scenarios
  • Angular - node (v12.1.0, x64)
  • Angular - node (v8.9.0, x64)
  • Angular - node (v8.9.0, x86)
  • Angular - node (v9.0.0, x64)
  • Angular - node (v9.0.0, x86)
  • Monaco - node (v12.1.0, x64)
  • Monaco - node (v8.9.0, x64)
  • Monaco - node (v8.9.0, x86)
  • Monaco - node (v9.0.0, x64)
  • Monaco - node (v9.0.0, x86)
  • TFS - node (v12.1.0, x64)
  • TFS - node (v8.9.0, x64)
  • TFS - node (v8.9.0, x86)
  • TFS - node (v9.0.0, x64)
  • TFS - node (v9.0.0, x86)
Benchmark Name Iterations
Current 33434 10
Baseline master 10

@typescript-bot
Copy link
Collaborator

The user suite test run you requested has finished and failed. I've opened a PR with the baseline diff from master.

@jack-williams
Copy link
Collaborator Author

User tests look like noise, I think.

@jack-williams jack-williams force-pushed the refactor-typeof-narrowing branch from 8be155d to 17ed7e2 Compare September 15, 2019 10:23
@fatcerberus
Copy link

fatcerberus commented Sep 15, 2019

In looking to resolve #33180 I want something like if (typeof x === "boolean") { ... } to narrow x to boolean when x is of type boolean | void

While this narrowing does match the reality at runtime, I'm not sure it's sound semantically, given that void represents random garbage to be discarded--the dual of any, as discussed in #33180 and also mentioned here.

IMO, any construct that allows a value of type void to be observed is inherently unsound and the type system should aim to prevent it whenever possible. By this I mean:

declare let callback: () => void = () => true;
declare let knownToBeBool;

declare function foo(b: boolean): void;

let x = Math.random() > 0.5 ? callback() : knownToBeBool;
if (typeof x === 'boolean') {
    // 'x' should NOT be narrowed here because it could leak the return value of 'callback'
    // (which we've already promised to ignore by declaring it 'void')
    foo(x);
}

@jack-williams
Copy link
Collaborator Author

jack-williams commented Sep 15, 2019

The void type is not dual to any, if it were then void would not be assignable to itself. The closest thing that resembles the dual of any is the invalid type that has been proposed.

Care needs to be taken with empty intersection reduction of void, which I’m not sure is entirely correct right now, but the principle of narrowing void in general does not seem unsound - at least in relation to the typical understanding of soundness.

@fatcerberus
Copy link

Quibbles about theoretical details aside (suffice to say there's no practical reason to disallow void = void assignments, duality notwithstanding), the point I made above stands: if a value possibly originates from the return value of a function type returning void, the type system should not allow that value to be observed (if it can be avoided) because the type promises that the value will be thrown away.

@fatcerberus
Copy link

fatcerberus commented Sep 15, 2019

In other words:

declare function doSomething(callback: () => void);
doSomething(someRandoFunction);

Since someRandoFunction is allowed to return anything at all, I view () => void as an implicit promise that regardless of what it returns, it won't affect the outcome of doSomething. That, to me, seems like the only sane interpretation as otherwise I’d just declare it => unknown (or any).

@fatcerberus
Copy link

The void type is not dual to any, if it were then void would not be assignable to itself.

Not related to this issue, but this just got me thinking, how much real-world code would break if this were actually the case... because if that change were made, void could probably work as invalid without adding a new keyword.

Function types returning void would still have to be assignable to each other though, so it might get tricky.

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Sep 15, 2019

The invalid proposal needs custom error messages, though =P

void does not give you that

@jack-williams
Copy link
Collaborator Author

If the proposed behaviour does not introduce significant real-world breaks I think it could be justified, though I don't know the landscape well enough. The behaviour feels slightly draconian though, given that even unconstrained type parameters can be inspected and narrowed.

The priority should be addressing the unsound behaviour that narrows void as if it were only inhabited by null or undefined---this is clearly wrong.

In modern TypeScript with undefined and unknown I'm not sure I really see the point of void anymore, but that is for a different thread.

@fatcerberus
Copy link

fatcerberus commented Sep 15, 2019

IMO void as the type of a value doesn’t really make sense. But it is useful to be able to express “I want a function whose return value I don’t care about” and have the type system be able to back me up on that (i.e. if I say I don’t care but the code I write proves that I do, that should be a type error). That can’t happen if the function is declared as returning, say, unknown—but using undefined here is overconstraining.

Put another way, We can think of () = void as meaning that the “output arity” of the function is zero.

@fatcerberus
Copy link

To cut through the noise and be clear (and get back on-topic): I only mean to argue that narrowing boolean | void to boolean is unsafe for the same reason as the current narrowing behavior w.r.t. undefined.

@jack-williams
Copy link
Collaborator Author

But it is useful to be able to express “I want a function whose return value I don’t care about” and have the type system be able to back me up on that (i.e. if I say I don’t care but the code I write proves that I do, that should be a type error). That can’t happen if the function is declared as returning, say, unknown—but using undefined here is overconstraining.

If this really is a desirable and bug-reducing feature I think the correct thing is to introduce non-narrowable type parameters and use generics.

I only mean to argue that narrowing boolean | void to boolean is unsafe for the same reason as the current narrowing behavior w.r.t. undefined.

There may be similarities when interpreting the contract of void as some form of obfuscation, but in terms of runtime safety I think they are different. The linked issue that removed void under the predicate !== undefined is fundamentally misleading, and will lead to situations where you are told you have a string, but really you have a number.

Narrowing void under typeof is not misleading in this sense: if something has the runtime type of "boolean" then it satisfies the type boolean.

The former is clearly unsound under any reasonably definition and should be fixed. The latter is open to interpretation.

@fatcerberus
Copy link

fatcerberus commented Sep 15, 2019

The way I see void is as an unsound bottom type (hence the perceived duality with the unsound top type, any): it represents a type with no inhabitants like never, but is inherently unsound because the “value-producing” logic is still reachable. Essentially: A bottom type that acts like a unit type.

Therefore, boolean | void can be seen as isomorphic to Maybe<boolean> (see example in my comment above), however this is merely an illusion since we can’t really have a simply-typed variable that has no value. The best we can do to maintain the illusion is treat the void as random noise - and if it’s random noise, there’s no reliable way to distinguish it from a valid value of type boolean. You're below the noise floor at that point.

Obviously, yes, if typeof x === 'boolean' then we know for a fact x is a Boolean value at runtime. But it may in fact be a Boolean value we've already promised to ignore, and therein lies the rub.

I’ll see if I can come up with a practical example later, so we’re not dealing entirely with intangible theory. If so I’ll also open a separate issue for discussion and stop spamming this one. 😄

@ethanresnick
Copy link
Contributor

Possibly relevant: #32809

@sandersn sandersn added the For Backlog Bug PRs that fix a backlog bug label Feb 1, 2020
@jack-williams
Copy link
Collaborator Author

I'll get to merging this in the next week (for anyone planning to review this).

Copy link
Member

@weswigham weswigham left a comment

Choose a reason for hiding this comment

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

The diff is a little hard to read because of the reordering, but this just looks like a relatively small refactoring. It does need a merge though.

@jack-williams
Copy link
Collaborator Author

It does need a merge though.

Will do!

@sandersn
Copy link
Member

sandersn commented May 6, 2020

@jack-williams @weswigham should we close this now? I wasn't sure from the discussion whether it's completely obsoleted by #38311 but it sounds like it.

@jack-williams
Copy link
Collaborator Author

jack-williams commented May 6, 2020

@sandersn The only objective here is to unify the logic of typeof narrowing in switch and if by sharing the code; there should be no functional change with this PR.

#38311 changes the root type for discriminant narrowing, so I think it solves a different problem. I did have another PR which was correctly replaced.

I've merged master and moved things around to reduce the diff, hopefully this makes it clearer.

Feel free to close or merge, whatever you see fit. I think we should get this out of the open PR's either way!

Tests fail because of n/n-1 with linter in master.

@jack-williams jack-williams force-pushed the refactor-typeof-narrowing branch from 913cd6d to 76e9b26 Compare May 6, 2020 19:56
@sandersn sandersn merged commit 05d59a1 into microsoft:master May 6, 2020
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
Development

Successfully merging this pull request may close these issues.

Inconsistency with boolean negation as void type guards with --strictNullChecks
7 participants