Skip to content

Unify logic in typeof narrowing - #33434

Merged
Nathan Shively-Sanders (sandersn) merged 2 commits into
microsoft:masterfrom
jack-williams:refactor-typeof-narrowing
May 6, 2020
Merged

Unify logic in typeof narrowing#33434
Nathan Shively-Sanders (sandersn) merged 2 commits into
microsoft:masterfrom
jack-williams:refactor-typeof-narrowing

Conversation

@jack-williams

@jack-williamsJack Williams (jack-williams) commented Sep 14, 2019

Copy link
Copy Markdown
Collaborator

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
Copy Markdown
CollaboratorAuthor

TypeScript Bot (@typescript-bot) test this
TypeScript Bot (@typescript-bot) user test this
TypeScript Bot (@typescript-bot) perf test this

because bots don't get weekends.

@typescript-bot

TypeScript Bot (typescript-bot) commented Sep 14, 2019

Copy link
Copy Markdown
Contributor

Heya Jack Williams (@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

TypeScript Bot (typescript-bot) commented Sep 14, 2019

Copy link
Copy Markdown
Contributor

Heya Jack Williams (@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

TypeScript Bot (typescript-bot) commented Sep 14, 2019

Copy link
Copy Markdown
Contributor

Heya Jack Williams (@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
Copy Markdown
Contributor

Jack Williams (@jack-williams)
The results of the perf run you requested are in!

Here they are:

Comparison Report - master..33434

Metricmaster33434DeltaBestWorst
Angular - node (v12.1.0, x64)
Memory used331,205k (± 0.06%)331,611k (± 0.02%)+406k (+ 0.12%)331,431k331,787k
Parse Time1.56s (± 0.44%)1.56s (± 0.48%)+0.00s (+ 0.26%)1.54s1.57s
Bind Time0.78s (± 0.00%)0.78s (± 0.61%)+0.00s (+ 0.38%)0.78s0.80s
Check Time4.26s (± 0.58%)4.29s (± 0.47%)+0.02s (+ 0.59%)4.24s4.34s
Emit Time5.25s (± 1.18%)5.27s (± 0.96%)+0.02s (+ 0.34%)5.17s5.40s
Total Time11.85s (± 0.68%)11.90s (± 0.37%)+0.05s (+ 0.41%)11.81s11.99s
Monaco - node (v12.1.0, x64)
Memory used345,970k (± 0.01%)345,931k (± 0.02%)-39k (- 0.01%)345,827k346,092k
Parse Time1.23s (± 0.54%)1.22s (± 0.85%)-0.01s (- 0.41%)1.20s1.24s
Bind Time0.67s (± 0.71%)0.67s (± 0.92%)-0.00s (- 0.30%)0.65s0.68s
Check Time4.25s (± 0.39%)4.24s (± 0.29%)-0.00s (- 0.05%)4.22s4.27s
Emit Time2.83s (± 0.68%)2.85s (± 0.98%)+0.02s (+ 0.81%)2.80s2.93s
Total Time8.97s (± 0.21%)8.98s (± 0.31%)+0.01s (+ 0.17%)8.94s9.06s
TFS - node (v12.1.0, x64)
Memory used301,424k (± 0.01%)301,421k (± 0.01%)-4k (- 0.00%)301,353k301,514k
Parse Time0.95s (± 0.58%)0.95s (± 0.74%)-0.00s (- 0.11%)0.94s0.97s
Bind Time0.62s (± 1.21%)0.62s (± 1.07%)-0.00s (- 0.16%)0.61s0.64s
Check Time3.85s (± 0.40%)3.86s (± 0.29%)+0.02s (+ 0.47%)3.84s3.89s
Emit Time2.99s (± 1.53%)2.96s (± 0.97%)-0.02s (- 0.80%)2.91s3.04s
Total Time8.40s (± 0.64%)8.40s (± 0.43%)-0.00s (- 0.06%)8.33s8.48s
Angular - node (v8.9.0, x64)
Memory used350,106k (± 0.02%)350,466k (± 0.01%)+360k (+ 0.10%)350,353k350,589k
Parse Time2.09s (± 0.49%)2.11s (± 0.25%)+0.02s (+ 0.86%)2.10s2.12s
Bind Time0.83s (± 0.78%)0.84s (± 0.95%)+0.01s (+ 0.72%)0.82s0.86s
Check Time5.11s (± 0.59%)5.13s (± 0.58%)+0.01s (+ 0.27%)5.05s5.20s
Emit Time5.99s (± 1.00%)5.98s (± 1.13%)-0.01s (- 0.25%)5.86s6.14s
Total Time14.03s (± 0.45%)14.05s (± 0.58%)+0.02s (+ 0.17%)13.87s14.19s
Monaco - node (v8.9.0, x64)
Memory used363,734k (± 0.02%)363,752k (± 0.01%)+18k (+ 0.00%)363,695k363,841k
Parse Time1.56s (± 0.57%)1.56s (± 0.30%)-0.00s (- 0.06%)1.55s1.57s
Bind Time0.89s (± 0.63%)0.89s (± 0.75%)+0.00s (+ 0.56%)0.88s0.91s
Check Time5.08s (± 1.43%)5.08s (± 1.46%)+0.00s (+ 0.08%)5.01s5.37s
Emit Time3.20s (± 4.18%)3.29s (± 2.27%)+0.09s (+ 2.69%)2.99s3.34s
Total Time10.73s (± 0.72%)10.82s (± 0.26%)+0.10s (+ 0.90%)10.76s10.88s
TFS - node (v8.9.0, x64)
Memory used317,699k (± 0.01%)317,716k (± 0.02%)+17k (+ 0.01%)317,570k317,800k
Parse Time1.26s (± 0.79%)1.26s (± 0.44%)-0.00s (- 0.16%)1.25s1.27s
Bind Time0.68s (± 5.26%)0.69s (± 4.95%)+0.00s (+ 0.15%)0.65s0.78s
Check Time4.45s (± 1.03%)4.43s (± 1.36%)-0.02s (- 0.45%)4.28s4.51s
Emit Time3.08s (± 0.48%)3.06s (± 0.67%)-0.02s (- 0.75%)3.00s3.11s
Total Time9.48s (± 0.35%)9.44s (± 0.41%)-0.04s (- 0.45%)9.38s9.56s
Angular - node (v8.9.0, x86)
Memory used198,198k (± 0.02%)198,391k (± 0.03%)+192k (+ 0.10%)198,247k198,508k
Parse Time2.03s (± 0.66%)2.04s (± 0.73%)+0.01s (+ 0.64%)2.01s2.08s
Bind Time0.95s (± 0.36%)0.95s (± 0.82%)+0.00s (+ 0.42%)0.94s0.97s
Check Time4.65s (± 0.49%)4.67s (± 0.50%)+0.02s (+ 0.37%)4.61s4.73s
Emit Time5.64s (± 0.61%)5.66s (± 0.78%)+0.02s (+ 0.32%)5.59s5.82s
Total Time13.27s (± 0.35%)13.32s (± 0.39%)+0.05s (+ 0.37%)13.22s13.45s
Monaco - node (v8.9.0, x86)
Memory used203,231k (± 0.01%)203,240k (± 0.01%)+9k (+ 0.00%)203,203k203,322k
Parse Time1.62s (± 0.85%)1.61s (± 0.47%)-0.01s (- 0.62%)1.59s1.63s
Bind Time0.72s (± 0.82%)0.72s (± 0.72%)-0.00s (- 0.41%)0.71s0.73s
Check Time4.86s (± 0.38%)4.88s (± 0.40%)+0.02s (+ 0.41%)4.82s4.91s
Emit Time3.16s (± 0.53%)3.16s (± 0.41%)-0.00s (- 0.03%)3.12s3.19s
Total Time10.36s (± 0.27%)10.36s (± 0.17%)+0.00s (+ 0.02%)10.31s10.40s
TFS - node (v8.9.0, x86)
Memory used178,553k (± 0.01%)178,554k (± 0.01%)+2k (+ 0.00%)178,509k178,612k
Parse Time1.31s (± 0.45%)1.31s (± 0.31%)-0.00s (- 0.23%)1.30s1.32s
Bind Time0.64s (± 0.81%)0.64s (± 0.75%)-0.00s (- 0.31%)0.63s0.65s
Check Time4.29s (± 0.61%)4.29s (± 0.73%)+0.00s (+ 0.07%)4.20s4.35s
Emit Time2.87s (± 1.54%)2.86s (± 0.90%)-0.01s (- 0.35%)2.79s2.91s
Total Time9.11s (± 0.67%)9.10s (± 0.53%)-0.01s (- 0.10%)9.00s9.20s
Angular - node (v9.0.0, x64)
Memory used349,750k (± 0.02%)350,119k (± 0.02%)+369k (+ 0.11%)350,022k350,339k
Parse Time1.82s (± 0.44%)1.82s (± 0.46%)+0.00s (+ 0.06%)1.80s1.83s
Bind Time0.78s (± 0.64%)0.78s (± 0.94%)+0.00s (+ 0.39%)0.76s0.79s
Check Time4.87s (± 0.56%)4.87s (± 0.53%)+0.01s (+ 0.16%)4.82s4.94s
Emit Time5.78s (± 1.22%)5.74s (± 1.29%)-0.04s (- 0.74%)5.59s5.89s
Total Time13.24s (± 0.68%)13.21s (± 0.54%)-0.03s (- 0.21%)13.02s13.37s
Monaco - node (v9.0.0, x64)
Memory used363,360k (± 0.01%)363,521k (± 0.02%)+161k (+ 0.04%)363,327k363,643k
Parse Time1.31s (± 0.44%)1.32s (± 0.66%)+0.00s (+ 0.38%)1.30s1.34s
Bind Time0.84s (± 0.44%)0.84s (± 1.01%)-0.01s (- 0.71%)0.82s0.85s
Check Time4.87s (± 0.33%)4.95s (± 1.77%)+0.08s (+ 1.73%)4.84s5.13s
Emit Time3.36s (± 0.54%)3.21s (± 5.05%)-0.14s (- 4.26%)2.87s3.38s
Total Time10.38s (± 0.33%)10.32s (± 0.95%)-0.06s (- 0.56%)10.12s10.51s
TFS - node (v9.0.0, x64)
Memory used317,508k (± 0.01%)317,446k (± 0.01%)-61k (- 0.02%)317,365k317,503k
Parse Time1.04s (± 0.59%)1.04s (± 0.73%)+0.00s (+ 0.48%)1.02s1.06s
Bind Time0.62s (± 0.84%)0.62s (± 0.76%)+0.00s (+ 0.65%)0.62s0.64s
Check Time4.38s (± 0.43%)4.40s (± 0.41%)+0.02s (+ 0.34%)4.37s4.45s
Emit Time3.17s (± 1.80%)3.21s (± 1.00%)+0.03s (+ 0.98%)3.11s3.25s
Total Time9.21s (± 0.76%)9.27s (± 0.46%)+0.06s (+ 0.63%)9.15s9.34s
Angular - node (v9.0.0, x86)
Memory used198,395k (± 0.03%)198,522k (± 0.02%)+127k (+ 0.06%)198,429k198,658k
Parse Time1.73s (± 0.45%)1.72s (± 0.54%)-0.01s (- 0.40%)1.71s1.75s
Bind Time0.89s (± 0.73%)0.90s (± 1.24%)+0.00s (+ 0.45%)0.87s0.93s
Check Time4.34s (± 0.45%)4.35s (± 0.85%)+0.01s (+ 0.30%)4.31s4.47s
Emit Time5.49s (± 0.59%)5.55s (± 0.97%)+0.07s (+ 1.20%)5.46s5.71s
Total Time12.44s (± 0.32%)12.52s (± 0.55%)+0.08s (+ 0.61%)12.38s12.68s
Monaco - node (v9.0.0, x86)
Memory used203,301k (± 0.02%)203,319k (± 0.02%)+18k (+ 0.01%)203,236k203,439k
Parse Time1.34s (± 0.45%)1.34s (± 0.51%)0.00s ( 0.00%)1.33s1.36s
Bind Time0.65s (± 1.03%)0.64s (± 1.06%)-0.00s (- 0.46%)0.63s0.66s
Check Time4.67s (± 0.63%)4.69s (± 0.42%)+0.01s (+ 0.24%)4.65s4.75s
Emit Time3.07s (± 0.46%)3.09s (± 0.65%)+0.02s (+ 0.59%)3.04s3.12s
Total Time9.73s (± 0.26%)9.76s (± 0.27%)+0.03s (+ 0.28%)9.70s9.81s
TFS - node (v9.0.0, x86)
Memory used178,605k (± 0.01%)178,604k (± 0.02%)-0k (- 0.00%)178,539k178,720k
Parse Time1.06s (± 0.70%)1.06s (± 0.56%)+0.00s (+ 0.09%)1.05s1.08s
Bind Time0.57s (± 0.86%)0.58s (± 0.81%)+0.01s (+ 1.05%)0.57s0.59s
Check Time4.13s (± 0.58%)4.16s (± 0.30%)+0.03s (+ 0.73%)4.14s4.19s
Emit Time2.80s (± 1.06%)2.78s (± 1.08%)-0.01s (- 0.46%)2.72s2.83s
Total Time8.56s (± 0.56%)8.58s (± 0.47%)+0.02s (+ 0.28%)8.49s8.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)
BenchmarkNameIterations
Current3343410
Baselinemaster10

@typescript-bot

Copy link
Copy Markdown
Contributor

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
Copy Markdown
CollaboratorAuthor

User tests look like noise, I think.

@fatcerberus

Bruce Pascoe (fatcerberus) commented Sep 15, 2019

Copy link
Copy Markdown

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:

declareletcallback: ()=>void=()=>true;declareletknownToBeBool;declarefunctionfoo(b: boolean): void;letx=Math.random()>0.5 ? callback() : knownToBeBool;if(typeofx==='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

Jack Williams (jack-williams) commented Sep 15, 2019

Copy link
Copy Markdown
CollaboratorAuthor

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
Copy Markdown

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

Bruce Pascoe (fatcerberus) commented Sep 15, 2019

Copy link
Copy Markdown

In other words:

declarefunctiondoSomething(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
Copy Markdown

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 returningvoid would still have to be assignable to each other though, so it might get tricky.

@AnyhowStep

AnyhowStep commented Sep 15, 2019

Copy link
Copy Markdown
Contributor

The invalid proposal needs custom error messages, though =P

void does not give you that

@jack-williams

Copy link
Copy Markdown
CollaboratorAuthor

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

Bruce Pascoe (fatcerberus) commented Sep 15, 2019

Copy link
Copy Markdown

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
Copy Markdown

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
Copy Markdown
CollaboratorAuthor

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

Bruce Pascoe (fatcerberus) commented Sep 15, 2019

Copy link
Copy Markdown

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
Copy Markdown
Contributor

Possibly relevant: #32809

@jack-williams

Copy link
Copy Markdown
CollaboratorAuthor

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

Copy link
Copy Markdown
Member

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
Copy Markdown
CollaboratorAuthor

It does need a merge though.

Will do!

@sandersn

Copy link
Copy Markdown
Member

Jack Williams (@jack-williams)Wesley Wigham (@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

Jack Williams (jack-williams) commented May 6, 2020

Copy link
Copy Markdown
CollaboratorAuthor

Nathan Shively-Sanders (@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.

@sandersn
Nathan Shively-Sanders (sandersn) merged commit 05d59a1 into microsoft:masterMay 6, 2020
@microsoftMicrosoft (microsoft) locked as resolved and limited conversation to collaborators Oct 21, 2025
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

For Backlog BugPRs 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

@jack-williams@typescript-bot@fatcerberus@AnyhowStep@ethanresnick@sandersn@weswigham