Skip to content

Favor asserted type in type predicate narrowing - #50044

Merged
Anders Hejlsberg (ahejlsberg) merged 2 commits into
mainfrom
fix49988
Jul 27, 2022
Merged

Favor asserted type in type predicate narrowing#50044
Anders Hejlsberg (ahejlsberg) merged 2 commits into
mainfrom
fix49988

Conversation

@ahejlsberg

@ahejlsbergAnders Hejlsberg (ahejlsberg) commented Jul 25, 2022

Copy link
Copy Markdown
Member

With this PR we consistently favor the asserted type in a type predicate narrowing when both the argument type and the asserted type are subtypes of each other. We used to do this for non-union argument types, but not for unions. Fixes the issue reported here.

In an ideal world we'd fix this issue by having the subtype relationship only consider fresh object literal types subtypes of object types with index signatures, but unfortunately this is too much of a breaking change (I tried in #50013, but I will need to abandon that PR).

Adding a bit more context...

This is our inconsistent behavior in 4.7 (adding undefined shouldn't cause an error):

declarefunctionisObject(value: unknown): value is Record<string,unknown>;declareconstobj1: {};obj1;// {}if(isObject(obj1)){obj1;// Record<string, unknown>obj1['attr'];}obj1;// Record<string, unknown>declareconstobj2: {}|undefined;obj2;// {} | undefinedif(isObject(obj2)){obj2;// {}obj2['attr'];// Error!}obj2;// {} | undefined

In #49625 we changed both of the above to error. That's more consistent, but a breaking change with the new error. With this PR we instead consistently favor the asserted type:

declarefunctionisObject(value: unknown): value is Record<string,unknown>;declareconstobj1: {};obj1;// {}if(isObject(obj1)){obj1;// Record<string, unknown>obj1['attr'];}obj1;// Record<string, unknown>declareconstobj2: {}|undefined;obj2;// {} | undefinedif(isObject(obj2)){obj2;// Record<string, unknown>obj2['attr'];}obj2;// Record<string, unknown> | undefined

Note that one issue with favoring the asserted type is that CFA continues with that type after the conditional block. Though undesired, that's an effect of how CFA works in the face of mutual subtypes. This behavior was already present for singleton types, but now also extends to union types.

Fixes#49988 (at least partially).

@ahejlsberg

Copy link
Copy Markdown
MemberAuthor

@typescript-bot

TypeScript Bot (typescript-bot) commented Jul 25, 2022

Copy link
Copy Markdown
Contributor

Heya Anders Hejlsberg (@ahejlsberg), I've started to run the extended test suite on this PR at a9484d3. You can monitor the build here.

@typescript-bot

TypeScript Bot (typescript-bot) commented Jul 25, 2022

Copy link
Copy Markdown
Contributor

Heya Anders Hejlsberg (@ahejlsberg), I've started to run the abridged perf test suite on this PR at a9484d3. You can monitor the build here.

Update: The results are in!

@typescript-bot

TypeScript Bot (typescript-bot) commented Jul 25, 2022

Copy link
Copy Markdown
Contributor

Heya Anders Hejlsberg (@ahejlsberg), I've started to run the parallelized Definitely Typed test suite on this PR at a9484d3. You can monitor the build here.

@typescript-bot

TypeScript Bot (typescript-bot) commented Jul 25, 2022

Copy link
Copy Markdown
Contributor

Heya Anders Hejlsberg (@ahejlsberg), I've started to run the diff-based user code test suite on this PR at a9484d3. You can monitor the build here.

Update: The results are in!

@typescript-bot

Copy link
Copy Markdown
Contributor

Heya Anders Hejlsberg (@ahejlsberg), I've run the RWC suite on this PR - assuming you're on the TS core team, you can view the resulting diff here.

@typescript-bot

Copy link
Copy Markdown
Contributor

Anders Hejlsberg (@ahejlsberg)
Great news! no new errors were found between main..refs/pull/50044/merge

@ahejlsberg

Anders Hejlsberg (ahejlsberg) commented Jul 25, 2022

Copy link
Copy Markdown
MemberAuthor

One new error in the RWC test suite. In the ant-design project, there's this sequence:

if(!React.isValidElement(element)){return;}constcolumn: any={
...element.props,// New error here};

where element is of type ReactElement<any> | ReactText. The isValidElement type predicate narrows to ReactElement<unknown> which is a mutual subtype with ReactElement<any>. So we now go with the unknown and subsequently can't access through a property that previously had type any. This is consistent with what would happen for a singleton, but it's still a new error.

@typescript-bot

Copy link
Copy Markdown
Contributor

Anders Hejlsberg (@ahejlsberg)
The results of the perf run you requested are in!

Here they are:

Comparison Report - main..50044

Metricmain50044DeltaBestWorst
Angular - node (v14.15.1, x64)
Memory used335,955k (± 0.01%)335,942k (± 0.01%)-13k (- 0.00%)335,899k336,013k
Parse Time2.06s (± 0.64%)2.07s (± 0.69%)+0.01s (+ 0.53%)2.04s2.10s
Bind Time0.89s (± 0.58%)0.90s (± 0.53%)+0.01s (+ 0.67%)0.89s0.91s
Check Time5.82s (± 0.26%)5.82s (± 0.52%)-0.00s (- 0.09%)5.74s5.88s
Emit Time6.34s (± 0.52%)6.38s (± 0.63%)+0.04s (+ 0.65%)6.30s6.48s
Total Time15.12s (± 0.32%)15.17s (± 0.36%)+0.05s (+ 0.34%)15.03s15.28s
Compiler-Unions - node (v14.15.1, x64)
Memory used193,148k (± 0.01%)193,139k (± 0.01%)-10k (- 0.00%)193,077k193,191k
Parse Time0.85s (± 0.47%)0.85s (± 0.78%)+0.00s (+ 0.47%)0.84s0.87s
Bind Time0.57s (± 1.02%)0.57s (± 0.65%)+0.01s (+ 1.41%)0.57s0.58s
Check Time6.70s (± 0.50%)6.75s (± 0.49%)+0.06s (+ 0.84%)6.67s6.83s
Emit Time2.49s (± 0.48%)2.50s (± 0.75%)+0.01s (+ 0.40%)2.46s2.55s
Total Time10.60s (± 0.32%)10.68s (± 0.45%)+0.08s (+ 0.72%)10.56s10.78s
Monaco - node (v14.15.1, x64)
Memory used325,639k (± 0.01%)325,645k (± 0.01%)+6k (+ 0.00%)325,611k325,698k
Parse Time1.58s (± 0.48%)1.59s (± 0.47%)+0.01s (+ 0.70%)1.57s1.60s
Bind Time0.78s (± 0.67%)0.78s (± 0.67%)+0.00s (+ 0.00%)0.77s0.79s
Check Time5.68s (± 0.36%)5.66s (± 0.30%)-0.02s (- 0.39%)5.64s5.71s
Emit Time3.36s (± 0.72%)3.35s (± 0.60%)-0.01s (- 0.24%)3.31s3.39s
Total Time11.41s (± 0.28%)11.38s (± 0.26%)-0.02s (- 0.20%)11.32s11.47s
TFS - node (v14.15.1, x64)
Memory used288,811k (± 0.01%)288,819k (± 0.01%)+8k (+ 0.00%)288,773k288,867k
Parse Time1.31s (± 1.16%)1.35s (± 2.64%)+0.04s (+ 2.74%)1.30s1.42s
Bind Time0.78s (± 5.28%)0.76s (± 3.50%)🟩-0.03s (- 3.58%)0.73s0.86s
Check Time5.34s (± 0.37%)5.35s (± 0.51%)+0.01s (+ 0.09%)5.29s5.39s
Emit Time3.59s (± 2.08%)3.62s (± 2.05%)+0.03s (+ 0.81%)3.47s3.73s
Total Time11.03s (± 0.60%)11.07s (± 0.93%)+0.04s (+ 0.39%)10.86s11.29s
material-ui - node (v14.15.1, x64)
Memory used446,707k (± 0.01%)446,712k (± 0.01%)+4k (+ 0.00%)446,638k446,771k
Parse Time1.87s (± 0.37%)1.88s (± 0.52%)+0.02s (+ 0.91%)1.86s1.90s
Bind Time0.72s (± 1.17%)0.73s (± 1.19%)+0.01s (+ 1.25%)0.71s0.75s
Check Time13.14s (± 0.55%)13.24s (± 0.74%)+0.10s (+ 0.73%)13.03s13.44s
Emit Time0.00s (± 0.00%)0.00s (± 0.00%)0.00s ( NaN%)0.00s0.00s
Total Time15.73s (± 0.49%)15.85s (± 0.63%)+0.12s (+ 0.75%)15.62s16.06s
xstate - node (v14.15.1, x64)
Memory used541,283k (± 0.00%)541,417k (± 0.00%)+134k (+ 0.02%)541,346k541,489k
Parse Time2.60s (± 0.36%)2.62s (± 0.56%)+0.02s (+ 0.69%)2.59s2.65s
Bind Time1.15s (± 0.98%)1.15s (± 0.96%)+0.00s (+ 0.26%)1.12s1.17s
Check Time1.54s (± 0.32%)1.56s (± 0.44%)+0.01s (+ 0.78%)1.54s1.57s
Emit Time0.07s (± 3.14%)0.07s (± 3.14%)0.00s ( 0.00%)0.07s0.08s
Total Time5.36s (± 0.37%)5.39s (± 0.26%)+0.03s (+ 0.54%)5.36s5.42s
System
Machine Namets-ci-ubuntu
Platformlinux 4.4.0-210-generic
Architecturex64
Available Memory16 GB
Available Memory15 GB
CPUs4 × Intel(R) Core(TM) i7-4770 CPU @ 3.40GHz
Hosts
  • node (v14.15.1, x64)
Scenarios
  • Angular - node (v14.15.1, x64)
  • Compiler-Unions - node (v14.15.1, x64)
  • Monaco - node (v14.15.1, x64)
  • TFS - node (v14.15.1, x64)
  • material-ui - node (v14.15.1, x64)
  • xstate - node (v14.15.1, x64)
BenchmarkNameIterations
Current5004410
Baselinemain10

Developer Information:

Download Benchmark

@ahejlsberg

Copy link
Copy Markdown
MemberAuthor

The is-object DT error from #49988 is now gone, and DT test suite is clean except for a few $ExpectType changes in lodash. It defines these two functions:

interfaceLodashStatic{isFunction(value: any): value is (...args: any[])=>any;isNative(value: any): value is (...args: any[])=>any;}

In lodash-tests.ts these are applied to the union number | (() => void). This used to produce () => void but now produces (...args: any[]) => any. Seems right given that's the asserted type and the type you already get when narrowing the singleton () => void. Pretty easy $ExpectType fix in lodash-tests.ts.

@jakebailey

Copy link
Copy Markdown
Member

How would one write a type guard that pulls a function out of a union of types, then? It seems unfortunate to lose that information. Is this solely because () => void is assignable to (...args: any[]) => any and vice versa?

@ahejlsberg

Copy link
Copy Markdown
MemberAuthor

How would one write a type guard that pulls a function out of a union of types, then?

isFunction(value: any): value is Function;

It's shorter, clearer, and does what you want, i.e. keeps the argument type for any function type, otherwise produces type Function. Reason being that every function type is a subtype of Function, but not vice versa.

// because that is the asserted type, but t for `instanceof` because generics aren't reflected in
// prototype object types.
const directlyRelated = mapType(matching || type, checkDerived ?
t => isTypeDerivedFrom(t, c) ? t : isTypeDerivedFrom(c, t) ? c : neverType :

@DanielRosenwasserDaniel Rosenwasser (DanielRosenwasser)Jul 26, 2022

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.

Why do you need the new isTypeDerivedFrom checks too?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

The isTypeDerivedFrom check isn't new. It's just that I'm calling it directly instead of indirectly through the isRelated local. Once we know checkDerived is true, we also know that isRelated references isTypeDerivedFrom, so might as well call it directly.

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Author: TeamFor Uncommitted BugPR for untriaged, rejected, closed or missing bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

#49625 breaks node type expectations

5 participants

@ahejlsberg@typescript-bot@jakebailey@sandersn@DanielRosenwasser