Skip to content

Infer type predicates from function bodies using control flow analysis - #57465

Merged
Ryan Cavanaugh (RyanCavanaugh) merged 43 commits into
microsoft:mainfrom
danvk:infer-type-predicate-16069
Mar 15, 2024
Merged

Infer type predicates from function bodies using control flow analysis#57465
Ryan Cavanaugh (RyanCavanaugh) merged 43 commits into
microsoft:mainfrom
danvk:infer-type-predicate-16069

Conversation

@danvk

@danvkDan Vanderkam (danvk) commented Feb 21, 2024

Copy link
Copy Markdown
Contributor

Fixes#16069
Fixes#38390
Fixes#10734
Fixes#50734
Fixes#12798

This PR uses the TypeScript's existing control flow analysis to infer type predicates for boolean-returning functions where appropriate. For example:

functionisString(x: string|number){returntypeofx==='string';}

This currently has an inferred return type of boolean, but with this PR it becomes a type predicate:

image

I filed #16069 seven years ago (!) and thought it would be interesting to try and fix it. It turned out to be cleaner and simpler than I thought: only ~65 LOC in one new function. I think it's a nice win!

How this works

A function is a candidate for an inferred type guard if:

  1. It does not have an explicit return type or type predicate.
  2. Its inferred return type is boolean.
  3. It has a single return statement and no implicit returns (this could potentially be relaxed later).
  4. It does not mutate its parameter.

If so, then the function looks something like this:

functionf(p: T,p2: T2, ...){// ...returnexpr;}

For each parameter, this PR determine what its flow type would be in each branch if the function looked like this instead:

functionf(p: T,p2: T2, ...){// ...if(expr){p1;// trueType}}

if trueType != T then we have a candidate for a type predicate.

We still need to check what a false return value means because of the semantics of type predicates. If we have:

declarefunctionisString(x: string|number): x is string;

then x is a string if this function returns true. But if it returns false then xmust be a number. In other words, in order to be a type predicate, a function should return true if and only if the predicate holds.

We can test this directly by plugging in trueType to the synthesized if statement and seeing what's left in the else branch:

functionf(p: trueType,p2: T2, ...){// ...if(expr){p1;// trueType}else{p1;// never?}

If it's never then we've demonstrated the "only if" condition.

Note that the previous versions of this PR did slightly different checks. The original version had problems with subtypes and my initial fix made more calls to getFlowTypeOfReference than was necessary. This version directly tests the condition that we want.

Wins

TypeScript is now able to infer type guards in many places where it's convenient, e.g. calls to filter:

constnums=[12,"foo",23,"bar"].filter(x=>typeofx==='number');// ^? const nums: number[]

Since this piggybacks off of the existing flow type code, all forms of narrowing that TypeScript understands will work.

constfoos=[newFoo(),newBar(),newFoo(),newBar()].filter(x=>xinstanceofFoo);// ^? const foos: Foo[]

There are a few other non-obvious wins:

Type guards now flow

constisString=(o: string|undefined): o is string=>!!o;// - (o: string | undefined) => boolean// + (o: string | undefined) => o is stringconstsecondIsString=(o: string|undefined)=>myGuard(o);

They also compose in new ways:

// const isFooBar: (x: unknown) => x is Foo | BarconstisFooBar=(x: unknown)=>isFoo(x)||isBar(x);

This is a gentle nudge away from truthiness footguns

We don't infer a type guard here if you check for truthiness, only if you check for non-nullishness:

constnumsTruthy=[0,1,2,null,3].filter(x=>!!x);// ^? const numsTruthy: (number | null)[]constnumsNonNull=[0,1,2,null,3].filter(x=>x!==null);// ^? const numsNonNull: number[]

This is because of the false case: if the truthiness test returns false, then x could be 0. Until TypeScript can represent "numbers other than 0" or it has a way to return distinct type predicates for the true and false cases, there's nothing that can be inferred from the truthiness test here.

If you're working with object types, on the other hand, there is no footgun and a truthiness test will infer a predicate:

constdatesTruthy=[newDate(),null,newDate(),null].filter(d=>!!d);// ^? const datesTruthy: Date[]

This provides a tangible incentive to do non-null checks instead of truthiness checks in the cases where you should be doing that anyway, so I call this a win. Notably the example in the original issue tests for truthiness rather than non-null.

Type guards are more discoverable

Type predicates are an incredibly useful feature, but you'd never learn about them without reading the documentation or seeing one in a declaration file. Now you can discover them by inspecting symbols in your own code:

constisString=(x: unknown)=>typeofx==='string';// ^? const isString: (x: unknown) => x is string

This makes them feel like they're more a part of the language.

Inferred type guards in interfaces are checked

While this PR defers to explicit type predicates, it will check an inferred predicate in this case:

interfaceNumberInferrer{isNumber(x: number|string): x is number;}classInferrerimplementsNumberInferrer{isNumber(x: number|string){// this is checked!!!returntypeofx==='number';}}

Interesting cases

The identity function on booleans is, in theory, a type guard:

// boolId: (b: boolean): b is true?constboolId=(b: boolean)=>b;

This seems correct but not very useful: why not just test the boolean? I've specifically prohibited inferring type predicates on boolean parameters in this PR. If nothing else this significantly reduces the number of diffs in the baselines.

Here's another interesting case:

functionflakyIsString(x: string|number){returntypeofx==='string'&&Math.random()>0.5;}

If this returns true then x is a string. But if it returns false then x could still be a string. So it would not be valid to infer a type predicate in this case. This is why we can't just check the trueType. In general, combining conditions like this will prevent inference of a type predicate. It would be nice if there were a way for a function to return distinct type predicates for the true and false cases (#15048). This would make inference much more powerful. But that would be a bigger change.

I remember RyanC saying once that a function's return type shouldn't be a restatement of its implementation in the type system. In some cases this can feel like a move in that direction:

// function hasAB(x: unknown): x is object & Record<"a", unknown> & Record<"b", unknown>functionhasAB(x: unknown){returnx!==null&&typeofx==='object'&&'a'inx&&'b'inx;}

Like any function, a type guard can be called with any subtype of its declared parameter types. We need to consider this when inferring a type guard:

functionisShortString(x: unknown){returntypeofx==='string'&&x.length<10;}

The issue here is that x narrows to string and unknown if you inline this check in an if / else, and Exclude<unknown, string> = unknown. But we can't infer a type predicate here because isShortString could be called with a string. This broke the test originally used in this PR, see this comment.

A function could potentially narrow the parameter type before it gets to the return, say via an assertion:

functionassertAndPredicate(x: string|number|Date){if(xinstanceofDate){thrownewError();}returntypeofx==='string';}

It's debatable what we should do in this case. Inferring x is string isn't wrong but will produce imprecise types in the else branch (which will include Date). This PR plays it safe by not inferring a type predicate in this case, at the cost of an extra call to getFlowTypeOfReference.

Breaks to Existing Code

Most new errors in existing code are more or less elaborate variations on #38390 (comment)

consta=[1,"foo",2,"bar"].filter(x=>typeofx==="string");a.push(10);// ok now, error with my PR

In other words, this PR allows TS to infer a narrower type for an array or other variable, and then you do something that required the broader type: pushing, reassigning, calling indexOf. See this comment for a full run-down of the changes that TypeScript Bot (@typescript-bot) and I have found.

Performance

We're doing additional work to infer type predicates, so performance is certainly a concern. TypeScript Bot (@typescript-bot)found the most significant slowdown with Compiler-Unions, a +1.25% increase in Check time.

Some fraction of the slowdown is from additional work being done in getTypePredicateFromBody (my addition), but some is also because TypeScript is inferring more precise types in more places thanks to the newly-detected type guards.

If there are performance concerns with the current implementation, there are a few options for reducing its scope:

  • Only run this on arrow functions
  • Only run this on contextually-typed arrow functions
  • Only run this on contextually-typed arrow functions where the context would use the type predicate (e.g. Array.prototype.filter).

Possible extensions

There are a few possible extensions of this that I've chosen to keep out of scope for this PR:

  • Check explicit type predicates. This PR defers to explicit type predicates, but you could imagine checking them instead. This would bring some type safety to user-defined type guards, which are currently no safer than type assertions.
  • Infer assertion functions. If you throw instead of returning a boolean, it should be possible to perform an analogous form of inference. Infer AssertsIdentifier type predicates #58495
  • If Suggestion: one-sided or fine-grained type guards #15048 were implemented, we could infer type predicates in many, many more situations.
  • Infer type predicates on this. It's possible for a boolean-returning method to be a this predicate. This should be a straightforward extension of this PR.
  • Infer type predicates on functions that return something other than boolean. If dates is (Date|null)[], then dates.filter(d => d) is a fine way to filter the nulls. This PR makes you write dates.filter(d => !!d). I believe this is a limitation of type predicates in general, not of this PR.
  • Handle multiple returns. It should be possible to infer a type guard for this function, for example, but it would require more bookkeeping. Infer type predicates for functions with multiple returns #58154
functionisString(x: string|number){if(typeofx==='string'){returntrue;}returnfalse;}

@typescript-botTypeScript Bot (typescript-bot) added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Feb 21, 2024
@typescript-bot

Copy link
Copy Markdown
Contributor

The TypeScript team hasn't accepted the linked issue #16069. If you can get it accepted, this PR will have a better chance of being reviewed.

@danvkDan Vanderkam (danvk) left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I'll look into the self-check errors.

Comment threadsrc/compiler/checker.ts Outdated

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

This file is deleted because we now (correctly) infer a type guard that's compatible with Array.isArray, which makes the error go away!


// String escaping issue (please help!)
function dunderguard(__x: number | string) {
>dunderguard : (__x: number | string) => ___x is string

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Note the three _s on the type predicate. This is a bug caused by the param.name.escapedText as string type assertion. I'm not sure how to fix it.

Comment threadsrc/compiler/checker.ts Outdated
13: "let leaderStatus: boolean",
14: "let checkedLeaderStatus: boolean",
15: "function isLeaderGuard(g: RoyalGuard): boolean"
15: "function isLeaderGuard(g: RoyalGuard): g is LeadGuard"

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

This is correct, a type predicate now flows where it did not before.

@RyanCavanaugh

Copy link
Copy Markdown
Member

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

@typescript-bot

TypeScript Bot (typescript-bot) commented Feb 21, 2024

Copy link
Copy Markdown
Contributor

Heya Ryan Cavanaugh (@RyanCavanaugh), I've started to run the faster perf test suite on this PR at e2684f1. You can monitor the build here.

Update: The results are in!

@fatcerberus

Copy link
Copy Markdown

I'm thinking this will break things like...

letisNumberable=(x: string|number)=>typeofx==='number';isNumberable=x=>!isNaN(parseInt(String(x)));

...as a function returning boolean is not assignable to a type predicate. This example is pretty contrived, but might be relevant for class inheritance (i.e. legal subclasses become illegal because the base class method now gets inferred as a typeguard).

@typescript-bot

Copy link
Copy Markdown
Contributor

Ryan Cavanaugh (@RyanCavanaugh)
The results of the perf run you requested are in!

Here they are:

tsc

Comparison Report - baseline..pr
MetricbaselineprDeltaBestWorstp-value
Angular - node (v18.15.0, x64)
Memory used295,666k (± 0.01%)295,656k (± 0.01%)~295,638k295,678kp=0.630 n=6
Parse Time2.66s (± 0.24%)2.66s (± 0.28%)~2.65s2.67sp=0.718 n=6
Bind Time0.83s (± 0.99%)0.83s (± 1.18%)~0.82s0.85sp=0.383 n=6
Check Time8.26s (± 0.31%)8.26s (± 0.52%)~8.21s8.32sp=0.872 n=6
Emit Time7.11s (± 0.28%)7.12s (± 0.35%)~7.08s7.15sp=0.418 n=6
Total Time18.86s (± 0.20%)18.87s (± 0.32%)~18.82s18.96sp=0.809 n=6
Compiler-Unions - node (v18.15.0, x64)
Memory used193,496k (± 1.54%)196,079k (± 1.56%)~194,078k200,129kp=0.128 n=6
Parse Time1.36s (± 1.43%)1.36s (± 1.27%)~1.33s1.38sp=1.000 n=6
Bind Time0.72s (± 0.00%)0.72s (± 0.00%)~0.72s0.72sp=1.000 n=6
Check Time9.35s (± 0.47%)9.71s (± 0.26%)+0.36s (+ 3.89%)9.69s9.76sp=0.005 n=6
Emit Time2.61s (± 0.47%)2.66s (± 0.21%)+0.04s (+ 1.53%)2.65s2.66sp=0.004 n=6
Total Time14.05s (± 0.44%)14.45s (± 0.28%)+0.40s (+ 2.88%)14.41s14.51sp=0.005 n=6
Monaco - node (v18.15.0, x64)
Memory used347,475k (± 0.01%)347,539k (± 0.01%)+64k (+ 0.02%)347,509k347,566kp=0.005 n=6
Parse Time2.48s (± 0.47%)2.48s (± 0.66%)~2.46s2.50sp=0.568 n=6
Bind Time0.93s (± 0.44%)0.93s (± 0.00%)~0.93s0.93sp=0.405 n=6
Check Time6.92s (± 0.27%)7.03s (± 0.37%)+0.11s (+ 1.54%)7.00s7.07sp=0.005 n=6
Emit Time4.04s (± 0.34%)4.06s (± 0.43%)~4.03s4.07sp=0.161 n=6
Total Time14.37s (± 0.12%)14.50s (± 0.16%)+0.13s (+ 0.88%)14.47s14.53sp=0.005 n=6
TFS - node (v18.15.0, x64)
Memory used302,868k (± 0.00%)302,836k (± 0.01%)-33k (- 0.01%)302,784k302,864kp=0.016 n=6
Parse Time2.02s (± 0.70%)2.01s (± 0.58%)~2.00s2.03sp=0.161 n=6
Bind Time1.00s (± 0.75%)1.01s (± 0.97%)~1.00s1.02sp=0.300 n=6
Check Time6.35s (± 0.31%)6.34s (± 0.46%)~6.29s6.37sp=0.627 n=6
Emit Time3.60s (± 0.29%)3.59s (± 0.43%)~3.57s3.60sp=0.797 n=6
Total Time12.96s (± 0.18%)12.94s (± 0.23%)~12.91s12.98sp=0.222 n=6
material-ui - node (v18.15.0, x64)
Memory used511,290k (± 0.00%)511,308k (± 0.01%)~511,283k511,383kp=0.296 n=6
Parse Time2.66s (± 0.46%)2.65s (± 0.75%)~2.63s2.68sp=0.557 n=6
Bind Time1.00s (± 0.75%)0.99s (± 0.41%)~0.99s1.00sp=0.100 n=6
Check Time17.30s (± 0.67%)17.32s (± 0.38%)~17.19s17.36sp=0.467 n=6
Emit Time0.00s (± 0.00%)0.00s (± 0.00%)~0.00s0.00sp=1.000 n=6
Total Time20.95s (± 0.52%)20.96s (± 0.32%)~20.84s21.02sp=0.377 n=6
mui-docs - node (v18.15.0, x64)
Memory used2,293,373k (± 0.00%)2,293,569k (± 0.00%)+196k (+ 0.01%)2,293,468k2,293,639kp=0.005 n=6
Parse Time11.98s (± 0.92%)11.96s (± 0.69%)~11.88s12.10sp=0.871 n=6
Bind Time2.65s (± 0.32%)2.64s (± 0.31%)~2.63s2.65sp=1.000 n=6
Check Time102.52s (± 0.59%)101.24s (± 0.98%)-1.29s (- 1.25%)99.81s102.66sp=0.045 n=6
Emit Time0.32s (± 1.28%)0.32s (± 1.60%)~0.32s0.33sp=0.114 n=6
Total Time117.47s (± 0.51%)116.16s (± 0.89%)-1.31s (- 1.12%)114.71s117.73sp=0.037 n=6
self-build-src - node (v18.15.0, x64)
Memory used2,413,189k (± 0.02%)868,471k (± 0.02%)🟩-1,544,717k (-64.01%)868,289k868,849kp=0.005 n=6
Parse Time4.90s (± 0.79%)5.50s (± 0.80%)🔻+0.60s (+12.27%)5.45s5.57sp=0.005 n=6
Bind Time1.86s (± 0.92%)2.34s (± 0.36%)🔻+0.48s (+25.96%)2.34s2.36sp=0.004 n=6
Check Time33.63s (± 0.43%)15.99s (± 0.39%)🟩-17.64s (-52.45%)15.90s16.08sp=0.005 n=6
Emit Time2.68s (± 1.18%)0.03s (± 0.00%)🟩-2.65s (-98.88%)0.03s0.03sp=0.003 n=6
Total Time43.08s (± 0.46%)23.88s (± 0.36%)🟩-19.20s (-44.57%)23.80s24.04sp=0.005 n=6
self-compiler - node (v18.15.0, x64)
Memory used418,962k (± 0.01%)414,976k (± 0.01%)-3,986k (- 0.95%)414,938k415,064kp=0.005 n=6
Parse Time2.82s (± 2.15%)2.79s (± 3.64%)~2.63s2.90sp=0.256 n=6
Bind Time1.10s (± 5.27%)1.12s (± 5.76%)~1.08s1.21sp=0.505 n=6
Check Time15.19s (± 0.25%)15.44s (± 0.28%)+0.26s (+ 1.69%)15.40s15.52sp=0.005 n=6
Emit Time1.13s (± 0.67%)0.02s (±18.82%)🟩-1.11s (-98.08%)0.02s0.03sp=0.003 n=6
Total Time20.23s (± 0.19%)19.37s (± 0.27%)🟩-0.86s (- 4.27%)19.31s19.45sp=0.005 n=6
vscode - node (v18.15.0, x64)
Memory used2,847,216k (± 0.00%)2,847,890k (± 0.00%)+674k (+ 0.02%)2,847,837k2,848,034kp=0.005 n=6
Parse Time10.78s (± 0.70%)10.74s (± 0.43%)~10.70s10.83sp=0.466 n=6
Bind Time3.43s (± 0.29%)3.43s (± 0.62%)~3.41s3.47sp=0.402 n=6
Check Time60.82s (± 0.46%)60.87s (± 0.37%)~60.50s61.15sp=0.873 n=6
Emit Time16.90s (± 8.34%)16.27s (± 0.42%)~16.16s16.36sp=0.126 n=6
Total Time91.93s (± 1.77%)91.31s (± 0.26%)~90.93s91.58sp=0.689 n=6
webpack - node (v18.15.0, x64)
Memory used395,919k (± 0.01%)396,017k (± 0.01%)+98k (+ 0.02%)395,982k396,095kp=0.005 n=6
Parse Time3.12s (± 0.33%)3.13s (± 0.26%)~3.12s3.14sp=0.112 n=6
Bind Time1.40s (± 0.58%)1.40s (± 0.37%)~1.39s1.40sp=0.752 n=6
Check Time14.02s (± 0.35%)14.13s (± 0.24%)+0.10s (+ 0.74%)14.08s14.18sp=0.006 n=6
Emit Time0.00s (± 0.00%)0.00s (± 0.00%)~0.00s0.00sp=1.000 n=6
Total Time18.55s (± 0.27%)18.66s (± 0.23%)+0.11s (+ 0.62%)18.60s18.72sp=0.005 n=6
xstate - node (v18.15.0, x64)
Memory used513,455k (± 0.01%)513,468k (± 0.01%)~513,404k513,507kp=0.630 n=6
Parse Time3.28s (± 0.37%)3.28s (± 0.19%)~3.27s3.29sp=0.403 n=6
Bind Time1.54s (± 0.71%)1.54s (± 0.49%)~1.53s1.55sp=0.604 n=6
Check Time2.87s (± 1.06%)2.85s (± 0.89%)~2.82s2.89sp=0.258 n=6
Emit Time0.08s (± 4.99%)0.07s (± 0.00%)🟩-0.01s (-14.29%)0.07s0.07sp=0.002 n=6
Total Time7.77s (± 0.45%)7.75s (± 0.40%)~7.72s7.80sp=0.334 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)
  • mui-docs - node (v18.15.0, x64)
  • self-build-src - node (v18.15.0, x64)
  • self-compiler - node (v18.15.0, x64)
  • vscode - node (v18.15.0, x64)
  • webpack - node (v18.15.0, x64)
  • xstate - node (v18.15.0, x64)
BenchmarkNameIterations
Currentpr6
Baselinebaseline6

Developer Information:

Download Benchmarks

@danvk

Dan Vanderkam (danvk) commented Feb 21, 2024

Copy link
Copy Markdown
ContributorAuthor

Bruce Pascoe (@fatcerberus) true, but that can also cut the other way! See the deleted errors file for javascriptThisAssignmentInStaticBlock.ts.

Ryan pointed out on another issue that inferring a type guard breaks this sort of code as well #38390 (comment)

consta=[1,"foo",2,"bar"].filter(x=>typeofx==="string");a.push(10);// ok now, error with my PR

I assume the +3.89% check time on compiler-unions is bad? Is there any information on how I can run this locally and see what's going on?

The CI/self-check found a flaw in my criteria for inferring a type predicate. I actually need to be even more strict! I should not infer a type predicate for this function, even though Exclude<unknown, string> = unknown.

functionisShortString(x: unknown){returntypeofx==='string'&&x.length<10;}declareletstr: string;if(isShortString(str)){str;// string}else{str;// never}

This is actually quite interesting. My approach has no way of distinguishing isShortString from:

functionisString(x: unknown){returntypeofx==='string';}

They both have initType=unknown, trueType=string and falseType=unknown. It would be valid to infer a type guard for isShortString if it were only ever called with unknown types. But if you call it with something like string or string | number then you can see that one is a valid type guard while the other is not.

I need to think a little more about whether this is fixable or if it's a flaw with this whole approach.

@fatcerberus

Copy link
Copy Markdown

Ouch, that's tricky. If there's a && present in the condition then you have to make sure there are no additional non-narrowing checks (or that the additional checks didn't narrow something else instead)

@danvk

Copy link
Copy Markdown
ContributorAuthor

Bruce Pascoe (@fatcerberus) yeah, see updated comment. I need to think about it a little more, but this might be a deal-breaker for this approach.

@fatcerberus

Bruce Pascoe (fatcerberus) commented Feb 21, 2024

Copy link
Copy Markdown

Negated types would be a great help here because the falseType would then be unknown & !string and the Exclude-based test would be sufficient.

nevermind, I'm dumb, you haven't ruled out all strings. ignore this

@danvk

Dan Vanderkam (danvk) commented Feb 21, 2024

Copy link
Copy Markdown
ContributorAuthor

I'm feeling somewhat hopeful that this approach can be salvaged. Instead of requiring that:

falseType = Exclude<initType, trueType>

what we actually need is:

falseType = Exclude<T, trueType> \forall T <: initType

i.e. that this relationship holds for all subtypes of the declared type. I'm hoping that in practice this just means I need to check that the relationship holds for T=initType and T=trueType, i.e. add one more check.

Again, this would be much easier if a function could return a different type predicate for the true and false cases!

@danvk

Copy link
Copy Markdown
ContributorAuthor

That fix seemed to work and all tests pass, so we should be back in business. This is going to be a bit slower than the version that TypeScript Bot (@typescript-bot)tested earlier but I'm feeling more confident that it's correct.

@jakebailey

Copy link
Copy Markdown
Member

TypeScript Bot (@typescript-bot) test top200
TypeScript Bot (@typescript-bot) user test this
TypeScript Bot (@typescript-bot) run dt

TypeScript Bot (@typescript-bot) perf test this faster
TypeScript Bot (@typescript-bot) pack this

@typescript-bot

TypeScript Bot (typescript-bot) commented Feb 21, 2024

Copy link
Copy Markdown
Contributor

Heya Jake Bailey (@jakebailey), I've started to run the tarball bundle task on this PR at d0e385e. You can monitor the build here.

@typescript-bot

TypeScript Bot (typescript-bot) commented Feb 21, 2024

Copy link
Copy Markdown
Contributor

Heya Jake Bailey (@jakebailey), I've started to run the diff-based user code test suite on this PR at d0e385e. You can monitor the build here.

Update: The results are in!

@typescript-bot

TypeScript Bot (typescript-bot) commented Feb 21, 2024

Copy link
Copy Markdown
Contributor

Heya Jake Bailey (@jakebailey), I've started to run the diff-based top-repos suite on this PR at d0e385e. You can monitor the build here.

Update: The results are in!

@typescript-bot

TypeScript Bot (typescript-bot) commented Feb 21, 2024

Copy link
Copy Markdown
Contributor

Heya Jake Bailey (@jakebailey), I've started to run the parallelized Definitely Typed test suite on this PR at d0e385e. You can monitor the build here.

Update: The results are in!

@typescript-bot

TypeScript Bot (typescript-bot) commented Feb 21, 2024

Copy link
Copy Markdown
Contributor

Heya Jake Bailey (@jakebailey), I've started to run the faster perf test suite on this PR at d0e385e. You can monitor the build here.

Update: The results are in!

@typescript-bot

TypeScript Bot (typescript-bot) commented Feb 21, 2024

Copy link
Copy Markdown
Contributor

Hey Jake Bailey (@jakebailey), 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/159974/artifacts?artifactName=tgz&fileId=6697AFDD16758083B1CD91AF9516D7A3DB7DCC2B13099E445566B57CD913996302&fileName=/typescript-5.5.0-insiders.20240221.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.5.0-pr-57465-17".;

@typescript-bot

Copy link
Copy Markdown
Contributor

Jake Bailey (@jakebailey) Here are the results of running the user test suite comparing main and refs/pull/57465/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

follow-redirects

/mnt/ts_downloads/follow-redirects/tsconfig.json

  • [NEW] error TS2345: Argument of type 'string | String' is not assignable to parameter of type 'string'.
    • /mnt/ts_downloads/follow-redirects/node_modules/follow-redirects/index.js(651,66)

puppeteer

packages/browsers/test/src/tsconfig.json

pyright

/mnt/ts_downloads/pyright/build.sh

  • [NEW] error TS2339: Property 'nodeType' does not exist on type 'never'.
    • /mnt/ts_downloads/pyright/pyright: ../pyright-internal/src/analyzer/testWalker.ts(78,47)
    • /mnt/ts_downloads/pyright/pyright-internal: src/analyzer/testWalker.ts(78,47)
    • /mnt/ts_downloads/pyright/vscode-pyright: ../pyright-internal/src/analyzer/testWalker.ts(78,47)

@typescript-bot

Copy link
Copy Markdown
Contributor

Jake Bailey (@jakebailey)
The results of the perf run you requested are in!

Here they are:

tsc

Comparison Report - baseline..pr
MetricbaselineprDeltaBestWorstp-value
Angular - node (v18.15.0, x64)
Memory used295,649k (± 0.01%)295,682k (± 0.01%)~295,641k295,730kp=0.109 n=6
Parse Time2.66s (± 0.15%)2.67s (± 0.21%)~2.66s2.67sp=0.282 n=6
Bind Time0.83s (± 0.49%)0.84s (± 0.97%)~0.83s0.85sp=0.056 n=6
Check Time8.26s (± 0.44%)8.28s (± 0.31%)~8.26s8.33sp=0.328 n=6
Emit Time7.12s (± 0.09%)7.12s (± 0.25%)~7.09s7.14sp=0.737 n=6
Total Time18.87s (± 0.19%)18.90s (± 0.19%)~18.87s18.97sp=0.291 n=6
Compiler-Unions - node (v18.15.0, x64)
Memory used191,595k (± 0.02%)195,558k (± 1.26%)+3,962k (+ 2.07%)194,102k200,105kp=0.005 n=6
Parse Time1.36s (± 0.80%)1.35s (± 2.08%)~1.32s1.39sp=0.453 n=6
Bind Time0.72s (± 0.00%)0.72s (± 0.00%)~0.72s0.72sp=1.000 n=6
Check Time9.36s (± 0.46%)9.78s (± 0.80%)🔻+0.42s (+ 4.45%)9.70s9.93sp=0.005 n=6
Emit Time2.62s (± 0.76%)2.65s (± 1.03%)~2.62s2.69sp=0.053 n=6
Total Time14.07s (± 0.28%)14.50s (± 0.56%)+0.44s (+ 3.10%)14.44s14.66sp=0.005 n=6
Monaco - node (v18.15.0, x64)
Memory used347,461k (± 0.00%)347,527k (± 0.00%)+67k (+ 0.02%)347,514k347,543kp=0.005 n=6
Parse Time2.48s (± 0.71%)2.48s (± 0.49%)~2.46s2.49sp=0.280 n=6
Bind Time0.93s (± 0.44%)0.93s (± 0.44%)~0.92s0.93sp=1.000 n=6
Check Time6.96s (± 0.66%)7.05s (± 0.59%)+0.09s (+ 1.32%)7.00s7.11sp=0.020 n=6
Emit Time4.06s (± 0.46%)4.06s (± 0.53%)~4.04s4.10sp=0.683 n=6
Total Time14.42s (± 0.40%)14.53s (± 0.20%)+0.11s (+ 0.76%)14.48s14.56sp=0.010 n=6
TFS - node (v18.15.0, x64)
Memory used302,868k (± 0.01%)302,848k (± 0.00%)~302,837k302,857kp=0.065 n=6
Parse Time2.01s (± 1.33%)2.01s (± 0.96%)~1.99s2.04sp=0.934 n=6
Bind Time1.00s (± 1.47%)1.01s (± 0.81%)~1.00s1.02sp=0.284 n=6
Check Time6.36s (± 0.30%)6.33s (± 0.42%)~6.29s6.37sp=0.139 n=6
Emit Time3.58s (± 0.33%)3.60s (± 0.27%)+0.02s (+ 0.56%)3.58s3.61sp=0.023 n=6
Total Time12.95s (± 0.24%)12.95s (± 0.24%)~12.90s12.99sp=0.872 n=6
material-ui - node (v18.15.0, x64)
Memory used511,290k (± 0.00%)511,320k (± 0.01%)~511,275k511,465kp=0.689 n=6
Parse Time2.65s (± 0.66%)2.65s (± 0.55%)~2.63s2.67sp=0.510 n=6
Bind Time1.00s (± 0.55%)0.99s (± 1.11%)~0.98s1.01sp=0.227 n=6
Check Time17.30s (± 0.43%)17.32s (± 0.37%)~17.20s17.37sp=0.629 n=6
Emit Time0.00s (± 0.00%)0.00s (± 0.00%)~0.00s0.00sp=1.000 n=6
Total Time20.94s (± 0.40%)20.96s (± 0.31%)~20.84s21.03sp=0.378 n=6
mui-docs - node (v18.15.0, x64)
Memory used2,293,496k (± 0.00%)2,293,665k (± 0.00%)+169k (+ 0.01%)2,293,606k2,293,713kp=0.005 n=6
Parse Time11.97s (± 0.86%)11.96s (± 1.02%)~11.85s12.17sp=0.808 n=6
Bind Time2.64s (± 0.50%)2.64s (± 0.15%)~2.63s2.64sp=0.446 n=6
Check Time102.54s (± 0.61%)101.79s (± 0.85%)~100.66s102.72sp=0.109 n=6
Emit Time0.32s (± 1.28%)0.32s (± 0.00%)~0.32s0.32sp=0.405 n=6
Total Time117.47s (± 0.50%)116.70s (± 0.79%)~115.57s117.84sp=0.128 n=6
self-build-src - node (v18.15.0, x64)
Memory used2,413,290k (± 0.02%)2,415,358k (± 0.02%)+2,068k (+ 0.09%)2,414,631k2,415,917kp=0.005 n=6
Parse Time4.92s (± 0.70%)4.91s (± 1.03%)~4.82s4.97sp=1.000 n=6
Bind Time1.86s (± 0.74%)1.87s (± 0.76%)~1.85s1.89sp=0.101 n=6
Check Time33.62s (± 0.34%)33.60s (± 0.45%)~33.46s33.89sp=0.471 n=6
Emit Time2.72s (± 1.73%)2.69s (± 1.09%)~2.66s2.73sp=0.197 n=6
Total Time43.14s (± 0.25%)43.10s (± 0.39%)~42.93s43.38sp=0.689 n=6
self-compiler - node (v18.15.0, x64)
Memory used418,986k (± 0.02%)419,551k (± 0.01%)+565k (+ 0.13%)419,479k419,588kp=0.005 n=6
Parse Time2.85s (± 0.65%)2.80s (± 2.36%)~2.67s2.85sp=0.059 n=6
Bind Time1.08s (± 0.51%)1.10s (± 5.36%)~1.07s1.22sp=0.476 n=6
Check Time15.16s (± 0.35%)15.38s (± 0.25%)+0.22s (+ 1.44%)15.31s15.42sp=0.005 n=6
Emit Time1.14s (± 1.02%)1.13s (± 1.12%)~1.12s1.15sp=0.140 n=6
Total Time20.22s (± 0.19%)20.41s (± 0.21%)+0.19s (+ 0.95%)20.34s20.45sp=0.005 n=6
vscode - node (v18.15.0, x64)
Memory used2,847,902k (± 0.00%)2,848,336k (± 0.00%)+434k (+ 0.02%)2,848,193k2,848,399kp=0.005 n=6
Parse Time10.74s (± 0.35%)10.76s (± 0.36%)~10.71s10.81sp=0.470 n=6
Bind Time3.43s (± 0.40%)3.43s (± 0.24%)~3.42s3.44sp=0.933 n=6
Check Time60.65s (± 0.90%)60.85s (± 0.19%)~60.63s60.97sp=0.092 n=6
Emit Time16.31s (± 0.60%)16.28s (± 0.59%)~16.17s16.42sp=0.521 n=6
Total Time91.13s (± 0.63%)91.32s (± 0.19%)~91.06s91.54sp=0.149 n=6
webpack - node (v18.15.0, x64)
Memory used395,939k (± 0.02%)396,031k (± 0.01%)+92k (+ 0.02%)395,955k396,088kp=0.020 n=6
Parse Time3.11s (± 1.07%)3.13s (± 0.77%)~3.10s3.16sp=0.418 n=6
Bind Time1.40s (± 0.54%)1.40s (± 0.84%)~1.38s1.41sp=0.933 n=6
Check Time14.05s (± 0.40%)14.15s (± 0.19%)+0.10s (+ 0.72%)14.12s14.20sp=0.005 n=6
Emit Time0.00s (± 0.00%)0.00s (± 0.00%)~0.00s0.00sp=1.000 n=6
Total Time18.56s (± 0.36%)18.68s (± 0.25%)+0.12s (+ 0.64%)18.62s18.76sp=0.010 n=6
xstate - node (v18.15.0, x64)
Memory used513,416k (± 0.00%)513,441k (± 0.01%)~513,364k513,501kp=0.173 n=6
Parse Time3.28s (± 0.26%)3.28s (± 0.16%)~3.27s3.28sp=0.533 n=6
Bind Time1.54s (± 0.26%)1.54s (± 0.00%)~1.54s1.54sp=0.405 n=6
Check Time2.88s (± 1.03%)2.84s (± 0.68%)-0.04s (- 1.45%)2.82s2.87sp=0.037 n=6
Emit Time0.08s (± 4.99%)0.07s (± 5.69%)🟩-0.01s (-12.24%)0.07s0.08sp=0.008 n=6
Total Time7.78s (± 0.38%)7.73s (± 0.24%)-0.05s (- 0.64%)7.72s7.77sp=0.013 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)
  • mui-docs - node (v18.15.0, x64)
  • self-build-src - node (v18.15.0, x64)
  • self-compiler - node (v18.15.0, x64)
  • vscode - node (v18.15.0, x64)
  • webpack - node (v18.15.0, x64)
  • xstate - node (v18.15.0, x64)
BenchmarkNameIterations
Currentpr6
Baselinebaseline6

Developer Information:

Download Benchmarks

@typescript-bot

Copy link
Copy Markdown
Contributor

Hey Jake Bailey (@jakebailey), the results of running the DT tests are ready.
There were interesting changes:

Branch only errors:

Package: lodash
Error:

Error: /home/vsts/work/1/DefinitelyTyped/types/lodash/lodash-tests.ts
1813:5 error TypeScript@local expected type to be:
string[]
got:
"a"[] @definitelytyped/expect
1814:5 error TypeScript@local expected type to be:
string[]
got:
"a"[] @definitelytyped/expect
✖ 2 problems (2 errors, 0 warnings)
at combineErrorsAndWarnings (/home/vsts/work/1/DefinitelyTyped/node_modules/.pnpm/@definitelytyped+dtslint@0.2.12_typescript@5.5.0-dev.20240221/node_modules/@definitelytyped/dtslint/dist/index.js:194:28)
at runTests (/home/vsts/work/1/DefinitelyTyped/node_modules/.pnpm/@definitelytyped+dtslint@0.2.12_typescript@5.5.0-dev.20240221/node_modules/@definitelytyped/dtslint/dist/index.js:186:20)

You can check the log here.

@typescript-bot

Copy link
Copy Markdown
Contributor

Jake Bailey (@jakebailey) Here are the results of running the top-repos suite comparing main and refs/pull/57465/merge:

Something interesting changed - please have a look.

Details

microsoft/vscode

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

src/tsconfig.json

src/tsconfig.tsec.json

prisma/prisma

80 of 115 projects failed to build with the old tsc and were ignored

packages/client/tsconfig.build.json

@JesusTheHun

Copy link
Copy Markdown

Some errors reported by the bot could be seen as mistakes in the user's code. Why the result of a array, filtered to be an array of numbers, should be typed as anything else than an array of number ?

Building on top of the current approach, but increasing complexity :

A "read" type and a "write" type, for mutable objects.

The read type would represent the latest known type at a point in the control flow. In the case of the array filter, this would mean "this array can hold strings and numbers, but at this point, it only holds numbers".
The write type is the type as we know it today.

This is clearly a whole new thing.

Bonus : at the end of the function, if the read type is the same as the write type, and the object is returned, then you can infer a type predicate. This part is out of the scope of your current PR, if I understood correctly.

@NWYLZW

YiJie (NWYLZW) commented Feb 22, 2024

Copy link
Copy Markdown

This idea is great! I seem to have come up with an implementation for fallback that doesn't require compilation support.

declareconstnotMatched: unique symbolfunctionisWhat<Input=unknown,T=never>(match: (input: Input,_: typeofnotMatched)=>T|typeofnotMatched): ((x: Input)=>x is [T]extends[Input] ? Input&T : never){return((x: any): boolean=>{try{returnmatch(x,notMatched)!==notMatched}catch(e){if([notMatched,void0,null,TypeError].includes(easany)){returnfalse}throwe}})asany}conststrs0=[1,'1',true].filter(// ^? string[]isWhat((t,_)=>typeoft==='string' ? t : _))conststrs1=[1,'1',true].filter(// ^? string[]isWhat(t=>{if(typeoft==='string')returntthrowvoid0}))

playground

However, this requires an import. But we can simplify the usage of this function by using webpack or built-in global functions.
If this pull request can be approved, it would be great. However, if it doesn't get approved, I think using this piece of code can still solve the current issue of the awkward usage of is.

@RDIL

Copy link
Copy Markdown

Hi, is it intentional that this doesn't work using .filter(Boolean)?

@jakebailey

Copy link
Copy Markdown
Member

Yes, Boolean is not a type predicate.

@ljharb

Copy link
Copy Markdown
Contributor

I would expect it to behave identically to x => !!x, since that’s what it does

@jcalz

Copy link
Copy Markdown
Contributor

Boolean as a type predicate is #16655 and unrelated to this feature (Boolean is not implemented in TypeScript, so TypeScript does not even have a chance to infer its return type as a type predicate. Instead, Boolean is just declared as something returning boolean. If it were declared to return a type predicate, then .filter(Boolean) would narrow in TS5.4 and below.)

@danvk

Copy link
Copy Markdown
ContributorAuthor

If it were declared to return a type predicate, then .filter(Boolean) would narrow in TS5.4 and below.)

In particular, see #50387 (comment) for an explanation of why this would have some surprising negative consequences.

@dest1n1s

Copy link
Copy Markdown

I'm feeling a bit confused about why a type guard for truthiness checking could not be performed. Currently I can filter out false types by manually add:

constisTrue=<T>(o: T|undefined|null|false|0|""): o is T=>!!o;constarray=[0,"",5,"hello",falseasconst,null,undefined]constresult: (string|number)[]=array.filter(isTrue)

but not with array.filter(v => !!v). Despite the fact that the above approach could not express that elements in result could not be "" or 0, it at least filters out false, null and undefined. It seems better to make v => !!v work the same as isTrue.

@danvk

Copy link
Copy Markdown
ContributorAuthor

Xuyang Ge (@dest1n1s) that version of isTrue should not be a type guard because it can lead to unsound types in the false case:

constisTrue=<T,>(o: T|undefined|null|false|0|""): o is T=>!!o;letx=Math.random()>0.5 ? 1 : 0;if(isTrue(x)){x// number -- ok}else{x// never -- wrong! could be 0!}

See my blog post, The The Hidden Side of Type Predicates. To safely infer a type guard for x => !!x for primitive types, we'd either need negated types or #15048.

@leppaott

leppaott commented Aug 1, 2024

Copy link
Copy Markdown
.filter((uuid) => uuid !== undefined)
// works
.filter((uuid) => uuid)
// doesn't work
.filter(Boolean)
// doesn't work

Any explanation why Boolean doesn't work? I see Boolean discussion how about just uuid i..e that can't be used also would filter zero-length strings.

@jcalz

Copy link
Copy Markdown
Contributor

Yes, we talked about it just a few comments back

@jcalz

Copy link
Copy Markdown
Contributor

Cross-linking to #42384 (feature request to narrow parent object by property guard)

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

Labels

For Uncommitted BugPR for untriaged, rejected, closed or missing bug

Projects

None yet

20 participants

@danvk@typescript-bot@RyanCavanaugh@fatcerberus@jakebailey@JesusTheHun@NWYLZW@DavidWeiss2@jacobshirley@MichalMarsalek@ehoogeveen-medweb@ahejlsberg@ljharb@craigphicks@weswigham@twentytwo777@MichaelMitchell-at@jcalz@Arctomachine@tshddx