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

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

Merged
merged 43 commits into from
Mar 15, 2024

Conversation

danvk
Copy link
Contributor

@danvk danvk commented Feb 21, 2024

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:

function isString(x: string | number) {
  return typeof x === '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:

function f(p: T, p2: T2, ...) {
  // ...
  return expr;
}

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

function f(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:

declare function isString(x: string | number): x is string;

then x is a string if this function returns true. But if it returns false then x must 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:

function f(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:

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

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

const foos = [new Foo(), new Bar(), new Foo(), new Bar()].filter(x => x instanceof Foo);
//    ^? const foos: Foo[]

There are a few other non-obvious wins:

Type guards now flow

const isString = (o: string | undefined): o is string => !!o;

// - (o: string | undefined) => boolean
// + (o: string | undefined) => o is string
const secondIsString = (o: string | undefined) => myGuard(o);

They also compose in new ways:

// const isFooBar: (x: unknown) => x is Foo | Bar
const isFooBar = (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:

const numsTruthy = [0, 1, 2, null, 3].filter(x => !!x);
//    ^? const numsTruthy: (number | null)[]
const numsNonNull = [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:

const datesTruthy = [new Date(), null, new Date(), 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:

const isString = (x: unknown) => typeof x === '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:

interface NumberInferrer {
  isNumber(x: number | string): x is number;
}
class Inferrer implements NumberInferrer {
  isNumber(x: number | string) {  // this is checked!!!
    return typeof x === 'number';
  }
}

Interesting cases

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

// boolId: (b: boolean): b is true?
const boolId = (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:

function flakyIsString(x: string | number) {
  return typeof x === '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>
function hasAB(x: unknown) {
  return x !== null && typeof x === 'object' && 'a' in x && 'b' in x;
}

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:

function isShortString(x: unknown) {
  return typeof x === '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:

function assertAndPredicate(x: string | number | Date) {
  if (x instanceof Date) {
    throw new Error();
  }
  return typeof x === '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)

const a = [1, "foo", 2, "bar"].filter(x => typeof x === "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 and I have found.

Performance

We're doing additional work to infer type predicates, so performance is certainly a concern. @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
function isString(x: string | number) {
  if (typeof x === 'string') {
    return true;
  }
  return false;
}

@typescript-bot typescript-bot added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Feb 21, 2024
@typescript-bot
Copy link
Collaborator

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.

Copy link
Contributor Author

@danvk danvk left a comment

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.

src/compiler/checker.ts Outdated Show resolved Hide resolved
Copy link
Contributor Author

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
Contributor Author

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.

src/compiler/checker.ts Show resolved Hide resolved
@@ -65,5 +65,5 @@ verify.quickInfos({
7: "(method) GuardInterface.isFollower(): this is FollowerGuard",
13: "let leaderStatus: boolean",
14: "let checkedLeaderStatus: boolean",
15: "function isLeaderGuard(g: RoyalGuard): boolean"
15: "function isLeaderGuard(g: RoyalGuard): g is LeadGuard"
Copy link
Contributor Author

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
Member

@typescript-bot perf test this faster

@typescript-bot
Copy link
Collaborator

typescript-bot commented Feb 21, 2024

Heya @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

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

let isNumberable = (x: string | number) => typeof x === '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
Collaborator

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

Here they are:

tsc

Comparison Report - baseline..pr
Metric baseline pr Delta Best Worst p-value
Angular - node (v18.15.0, x64)
Memory used 295,666k (± 0.01%) 295,656k (± 0.01%) ~ 295,638k 295,678k p=0.630 n=6
Parse Time 2.66s (± 0.24%) 2.66s (± 0.28%) ~ 2.65s 2.67s p=0.718 n=6
Bind Time 0.83s (± 0.99%) 0.83s (± 1.18%) ~ 0.82s 0.85s p=0.383 n=6
Check Time 8.26s (± 0.31%) 8.26s (± 0.52%) ~ 8.21s 8.32s p=0.872 n=6
Emit Time 7.11s (± 0.28%) 7.12s (± 0.35%) ~ 7.08s 7.15s p=0.418 n=6
Total Time 18.86s (± 0.20%) 18.87s (± 0.32%) ~ 18.82s 18.96s p=0.809 n=6
Compiler-Unions - node (v18.15.0, x64)
Memory used 193,496k (± 1.54%) 196,079k (± 1.56%) ~ 194,078k 200,129k p=0.128 n=6
Parse Time 1.36s (± 1.43%) 1.36s (± 1.27%) ~ 1.33s 1.38s 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.35s (± 0.47%) 9.71s (± 0.26%) +0.36s (+ 3.89%) 9.69s 9.76s p=0.005 n=6
Emit Time 2.61s (± 0.47%) 2.66s (± 0.21%) +0.04s (+ 1.53%) 2.65s 2.66s p=0.004 n=6
Total Time 14.05s (± 0.44%) 14.45s (± 0.28%) +0.40s (+ 2.88%) 14.41s 14.51s p=0.005 n=6
Monaco - node (v18.15.0, x64)
Memory used 347,475k (± 0.01%) 347,539k (± 0.01%) +64k (+ 0.02%) 347,509k 347,566k p=0.005 n=6
Parse Time 2.48s (± 0.47%) 2.48s (± 0.66%) ~ 2.46s 2.50s p=0.568 n=6
Bind Time 0.93s (± 0.44%) 0.93s (± 0.00%) ~ 0.93s 0.93s p=0.405 n=6
Check Time 6.92s (± 0.27%) 7.03s (± 0.37%) +0.11s (+ 1.54%) 7.00s 7.07s p=0.005 n=6
Emit Time 4.04s (± 0.34%) 4.06s (± 0.43%) ~ 4.03s 4.07s p=0.161 n=6
Total Time 14.37s (± 0.12%) 14.50s (± 0.16%) +0.13s (+ 0.88%) 14.47s 14.53s p=0.005 n=6
TFS - node (v18.15.0, x64)
Memory used 302,868k (± 0.00%) 302,836k (± 0.01%) -33k (- 0.01%) 302,784k 302,864k p=0.016 n=6
Parse Time 2.02s (± 0.70%) 2.01s (± 0.58%) ~ 2.00s 2.03s p=0.161 n=6
Bind Time 1.00s (± 0.75%) 1.01s (± 0.97%) ~ 1.00s 1.02s p=0.300 n=6
Check Time 6.35s (± 0.31%) 6.34s (± 0.46%) ~ 6.29s 6.37s p=0.627 n=6
Emit Time 3.60s (± 0.29%) 3.59s (± 0.43%) ~ 3.57s 3.60s p=0.797 n=6
Total Time 12.96s (± 0.18%) 12.94s (± 0.23%) ~ 12.91s 12.98s p=0.222 n=6
material-ui - node (v18.15.0, x64)
Memory used 511,290k (± 0.00%) 511,308k (± 0.01%) ~ 511,283k 511,383k p=0.296 n=6
Parse Time 2.66s (± 0.46%) 2.65s (± 0.75%) ~ 2.63s 2.68s p=0.557 n=6
Bind Time 1.00s (± 0.75%) 0.99s (± 0.41%) ~ 0.99s 1.00s p=0.100 n=6
Check Time 17.30s (± 0.67%) 17.32s (± 0.38%) ~ 17.19s 17.36s p=0.467 n=6
Emit Time 0.00s (± 0.00%) 0.00s (± 0.00%) ~ 0.00s 0.00s p=1.000 n=6
Total Time 20.95s (± 0.52%) 20.96s (± 0.32%) ~ 20.84s 21.02s p=0.377 n=6
mui-docs - node (v18.15.0, x64)
Memory used 2,293,373k (± 0.00%) 2,293,569k (± 0.00%) +196k (+ 0.01%) 2,293,468k 2,293,639k p=0.005 n=6
Parse Time 11.98s (± 0.92%) 11.96s (± 0.69%) ~ 11.88s 12.10s p=0.871 n=6
Bind Time 2.65s (± 0.32%) 2.64s (± 0.31%) ~ 2.63s 2.65s p=1.000 n=6
Check Time 102.52s (± 0.59%) 101.24s (± 0.98%) -1.29s (- 1.25%) 99.81s 102.66s p=0.045 n=6
Emit Time 0.32s (± 1.28%) 0.32s (± 1.60%) ~ 0.32s 0.33s p=0.114 n=6
Total Time 117.47s (± 0.51%) 116.16s (± 0.89%) -1.31s (- 1.12%) 114.71s 117.73s p=0.037 n=6
self-build-src - node (v18.15.0, x64)
Memory used 2,413,189k (± 0.02%) 868,471k (± 0.02%) 🟩-1,544,717k (-64.01%) 868,289k 868,849k p=0.005 n=6
Parse Time 4.90s (± 0.79%) 5.50s (± 0.80%) 🔻+0.60s (+12.27%) 5.45s 5.57s p=0.005 n=6
Bind Time 1.86s (± 0.92%) 2.34s (± 0.36%) 🔻+0.48s (+25.96%) 2.34s 2.36s p=0.004 n=6
Check Time 33.63s (± 0.43%) 15.99s (± 0.39%) 🟩-17.64s (-52.45%) 15.90s 16.08s p=0.005 n=6
Emit Time 2.68s (± 1.18%) 0.03s (± 0.00%) 🟩-2.65s (-98.88%) 0.03s 0.03s p=0.003 n=6
Total Time 43.08s (± 0.46%) 23.88s (± 0.36%) 🟩-19.20s (-44.57%) 23.80s 24.04s p=0.005 n=6
self-compiler - node (v18.15.0, x64)
Memory used 418,962k (± 0.01%) 414,976k (± 0.01%) -3,986k (- 0.95%) 414,938k 415,064k p=0.005 n=6
Parse Time 2.82s (± 2.15%) 2.79s (± 3.64%) ~ 2.63s 2.90s p=0.256 n=6
Bind Time 1.10s (± 5.27%) 1.12s (± 5.76%) ~ 1.08s 1.21s p=0.505 n=6
Check Time 15.19s (± 0.25%) 15.44s (± 0.28%) +0.26s (+ 1.69%) 15.40s 15.52s p=0.005 n=6
Emit Time 1.13s (± 0.67%) 0.02s (±18.82%) 🟩-1.11s (-98.08%) 0.02s 0.03s p=0.003 n=6
Total Time 20.23s (± 0.19%) 19.37s (± 0.27%) 🟩-0.86s (- 4.27%) 19.31s 19.45s p=0.005 n=6
vscode - node (v18.15.0, x64)
Memory used 2,847,216k (± 0.00%) 2,847,890k (± 0.00%) +674k (+ 0.02%) 2,847,837k 2,848,034k p=0.005 n=6
Parse Time 10.78s (± 0.70%) 10.74s (± 0.43%) ~ 10.70s 10.83s p=0.466 n=6
Bind Time 3.43s (± 0.29%) 3.43s (± 0.62%) ~ 3.41s 3.47s p=0.402 n=6
Check Time 60.82s (± 0.46%) 60.87s (± 0.37%) ~ 60.50s 61.15s p=0.873 n=6
Emit Time 16.90s (± 8.34%) 16.27s (± 0.42%) ~ 16.16s 16.36s p=0.126 n=6
Total Time 91.93s (± 1.77%) 91.31s (± 0.26%) ~ 90.93s 91.58s p=0.689 n=6
webpack - node (v18.15.0, x64)
Memory used 395,919k (± 0.01%) 396,017k (± 0.01%) +98k (+ 0.02%) 395,982k 396,095k p=0.005 n=6
Parse Time 3.12s (± 0.33%) 3.13s (± 0.26%) ~ 3.12s 3.14s p=0.112 n=6
Bind Time 1.40s (± 0.58%) 1.40s (± 0.37%) ~ 1.39s 1.40s p=0.752 n=6
Check Time 14.02s (± 0.35%) 14.13s (± 0.24%) +0.10s (+ 0.74%) 14.08s 14.18s p=0.006 n=6
Emit Time 0.00s (± 0.00%) 0.00s (± 0.00%) ~ 0.00s 0.00s p=1.000 n=6
Total Time 18.55s (± 0.27%) 18.66s (± 0.23%) +0.11s (+ 0.62%) 18.60s 18.72s p=0.005 n=6
xstate - node (v18.15.0, x64)
Memory used 513,455k (± 0.01%) 513,468k (± 0.01%) ~ 513,404k 513,507k p=0.630 n=6
Parse Time 3.28s (± 0.37%) 3.28s (± 0.19%) ~ 3.27s 3.29s p=0.403 n=6
Bind Time 1.54s (± 0.71%) 1.54s (± 0.49%) ~ 1.53s 1.55s p=0.604 n=6
Check Time 2.87s (± 1.06%) 2.85s (± 0.89%) ~ 2.82s 2.89s p=0.258 n=6
Emit Time 0.08s (± 4.99%) 0.07s (± 0.00%) 🟩-0.01s (-14.29%) 0.07s 0.07s p=0.002 n=6
Total Time 7.77s (± 0.45%) 7.75s (± 0.40%) ~ 7.72s 7.80s p=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)
Benchmark Name Iterations
Current pr 6
Baseline baseline 6

Developer Information:

Download Benchmarks

@danvk
Copy link
Contributor Author

danvk commented Feb 21, 2024

@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)

const a = [1, "foo", 2, "bar"].filter(x => typeof x === "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.

function isShortString(x: unknown) {
  return typeof x === 'string' && x.length < 10;
}

declare let str: string;
if (isShortString(str)) {
  str;  // string
} else {
  str;  // never
}

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

function isString(x: unknown) {
  return typeof x === '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

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
Contributor Author

danvk commented Feb 21, 2024

@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
Copy link

fatcerberus commented Feb 21, 2024

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
Copy link
Contributor Author

danvk commented Feb 21, 2024

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
Contributor Author

danvk commented Feb 21, 2024

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 tested earlier but I'm feeling more confident that it's correct.

@jakebailey
Copy link
Member

@typescript-bot test top200
@typescript-bot user test this
@typescript-bot run dt

@typescript-bot perf test this faster
@typescript-bot pack this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Feb 21, 2024

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

@typescript-bot
Copy link
Collaborator

typescript-bot commented Feb 21, 2024

Heya @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
Copy link
Collaborator

typescript-bot commented Feb 21, 2024

Heya @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
Copy link
Collaborator

typescript-bot commented Feb 21, 2024

Heya @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
Copy link
Collaborator

typescript-bot commented Feb 21, 2024

Heya @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
Copy link
Collaborator

typescript-bot commented Feb 21, 2024

Hey @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/[email protected]".;

@typescript-bot
Copy link
Collaborator

@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
Collaborator

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

Here they are:

tsc

Comparison Report - baseline..pr
Metric baseline pr Delta Best Worst p-value
Angular - node (v18.15.0, x64)
Memory used 295,649k (± 0.01%) 295,682k (± 0.01%) ~ 295,641k 295,730k p=0.109 n=6
Parse Time 2.66s (± 0.15%) 2.67s (± 0.21%) ~ 2.66s 2.67s p=0.282 n=6
Bind Time 0.83s (± 0.49%) 0.84s (± 0.97%) ~ 0.83s 0.85s p=0.056 n=6
Check Time 8.26s (± 0.44%) 8.28s (± 0.31%) ~ 8.26s 8.33s p=0.328 n=6
Emit Time 7.12s (± 0.09%) 7.12s (± 0.25%) ~ 7.09s 7.14s p=0.737 n=6
Total Time 18.87s (± 0.19%) 18.90s (± 0.19%) ~ 18.87s 18.97s p=0.291 n=6
Compiler-Unions - node (v18.15.0, x64)
Memory used 191,595k (± 0.02%) 195,558k (± 1.26%) +3,962k (+ 2.07%) 194,102k 200,105k p=0.005 n=6
Parse Time 1.36s (± 0.80%) 1.35s (± 2.08%) ~ 1.32s 1.39s p=0.453 n=6
Bind Time 0.72s (± 0.00%) 0.72s (± 0.00%) ~ 0.72s 0.72s p=1.000 n=6
Check Time 9.36s (± 0.46%) 9.78s (± 0.80%) 🔻+0.42s (+ 4.45%) 9.70s 9.93s p=0.005 n=6
Emit Time 2.62s (± 0.76%) 2.65s (± 1.03%) ~ 2.62s 2.69s p=0.053 n=6
Total Time 14.07s (± 0.28%) 14.50s (± 0.56%) +0.44s (+ 3.10%) 14.44s 14.66s p=0.005 n=6
Monaco - node (v18.15.0, x64)
Memory used 347,461k (± 0.00%) 347,527k (± 0.00%) +67k (+ 0.02%) 347,514k 347,543k p=0.005 n=6
Parse Time 2.48s (± 0.71%) 2.48s (± 0.49%) ~ 2.46s 2.49s p=0.280 n=6
Bind Time 0.93s (± 0.44%) 0.93s (± 0.44%) ~ 0.92s 0.93s p=1.000 n=6
Check Time 6.96s (± 0.66%) 7.05s (± 0.59%) +0.09s (+ 1.32%) 7.00s 7.11s p=0.020 n=6
Emit Time 4.06s (± 0.46%) 4.06s (± 0.53%) ~ 4.04s 4.10s p=0.683 n=6
Total Time 14.42s (± 0.40%) 14.53s (± 0.20%) +0.11s (+ 0.76%) 14.48s 14.56s p=0.010 n=6
TFS - node (v18.15.0, x64)
Memory used 302,868k (± 0.01%) 302,848k (± 0.00%) ~ 302,837k 302,857k p=0.065 n=6
Parse Time 2.01s (± 1.33%) 2.01s (± 0.96%) ~ 1.99s 2.04s p=0.934 n=6
Bind Time 1.00s (± 1.47%) 1.01s (± 0.81%) ~ 1.00s 1.02s p=0.284 n=6
Check Time 6.36s (± 0.30%) 6.33s (± 0.42%) ~ 6.29s 6.37s p=0.139 n=6
Emit Time 3.58s (± 0.33%) 3.60s (± 0.27%) +0.02s (+ 0.56%) 3.58s 3.61s p=0.023 n=6
Total Time 12.95s (± 0.24%) 12.95s (± 0.24%) ~ 12.90s 12.99s p=0.872 n=6
material-ui - node (v18.15.0, x64)
Memory used 511,290k (± 0.00%) 511,320k (± 0.01%) ~ 511,275k 511,465k p=0.689 n=6
Parse Time 2.65s (± 0.66%) 2.65s (± 0.55%) ~ 2.63s 2.67s p=0.510 n=6
Bind Time 1.00s (± 0.55%) 0.99s (± 1.11%) ~ 0.98s 1.01s p=0.227 n=6
Check Time 17.30s (± 0.43%) 17.32s (± 0.37%) ~ 17.20s 17.37s p=0.629 n=6
Emit Time 0.00s (± 0.00%) 0.00s (± 0.00%) ~ 0.00s 0.00s p=1.000 n=6
Total Time 20.94s (± 0.40%) 20.96s (± 0.31%) ~ 20.84s 21.03s p=0.378 n=6
mui-docs - node (v18.15.0, x64)
Memory used 2,293,496k (± 0.00%) 2,293,665k (± 0.00%) +169k (+ 0.01%) 2,293,606k 2,293,713k p=0.005 n=6
Parse Time 11.97s (± 0.86%) 11.96s (± 1.02%) ~ 11.85s 12.17s p=0.808 n=6
Bind Time 2.64s (± 0.50%) 2.64s (± 0.15%) ~ 2.63s 2.64s p=0.446 n=6
Check Time 102.54s (± 0.61%) 101.79s (± 0.85%) ~ 100.66s 102.72s p=0.109 n=6
Emit Time 0.32s (± 1.28%) 0.32s (± 0.00%) ~ 0.32s 0.32s p=0.405 n=6
Total Time 117.47s (± 0.50%) 116.70s (± 0.79%) ~ 115.57s 117.84s p=0.128 n=6
self-build-src - node (v18.15.0, x64)
Memory used 2,413,290k (± 0.02%) 2,415,358k (± 0.02%) +2,068k (+ 0.09%) 2,414,631k 2,415,917k p=0.005 n=6
Parse Time 4.92s (± 0.70%) 4.91s (± 1.03%) ~ 4.82s 4.97s p=1.000 n=6
Bind Time 1.86s (± 0.74%) 1.87s (± 0.76%) ~ 1.85s 1.89s p=0.101 n=6
Check Time 33.62s (± 0.34%) 33.60s (± 0.45%) ~ 33.46s 33.89s p=0.471 n=6
Emit Time 2.72s (± 1.73%) 2.69s (± 1.09%) ~ 2.66s 2.73s p=0.197 n=6
Total Time 43.14s (± 0.25%) 43.10s (± 0.39%) ~ 42.93s 43.38s p=0.689 n=6
self-compiler - node (v18.15.0, x64)
Memory used 418,986k (± 0.02%) 419,551k (± 0.01%) +565k (+ 0.13%) 419,479k 419,588k p=0.005 n=6
Parse Time 2.85s (± 0.65%) 2.80s (± 2.36%) ~ 2.67s 2.85s p=0.059 n=6
Bind Time 1.08s (± 0.51%) 1.10s (± 5.36%) ~ 1.07s 1.22s p=0.476 n=6
Check Time 15.16s (± 0.35%) 15.38s (± 0.25%) +0.22s (+ 1.44%) 15.31s 15.42s p=0.005 n=6
Emit Time 1.14s (± 1.02%) 1.13s (± 1.12%) ~ 1.12s 1.15s p=0.140 n=6
Total Time 20.22s (± 0.19%) 20.41s (± 0.21%) +0.19s (+ 0.95%) 20.34s 20.45s p=0.005 n=6
vscode - node (v18.15.0, x64)
Memory used 2,847,902k (± 0.00%) 2,848,336k (± 0.00%) +434k (+ 0.02%) 2,848,193k 2,848,399k p=0.005 n=6
Parse Time 10.74s (± 0.35%) 10.76s (± 0.36%) ~ 10.71s 10.81s p=0.470 n=6
Bind Time 3.43s (± 0.40%) 3.43s (± 0.24%) ~ 3.42s 3.44s p=0.933 n=6
Check Time 60.65s (± 0.90%) 60.85s (± 0.19%) ~ 60.63s 60.97s p=0.092 n=6
Emit Time 16.31s (± 0.60%) 16.28s (± 0.59%) ~ 16.17s 16.42s p=0.521 n=6
Total Time 91.13s (± 0.63%) 91.32s (± 0.19%) ~ 91.06s 91.54s p=0.149 n=6
webpack - node (v18.15.0, x64)
Memory used 395,939k (± 0.02%) 396,031k (± 0.01%) +92k (+ 0.02%) 395,955k 396,088k p=0.020 n=6
Parse Time 3.11s (± 1.07%) 3.13s (± 0.77%) ~ 3.10s 3.16s p=0.418 n=6
Bind Time 1.40s (± 0.54%) 1.40s (± 0.84%) ~ 1.38s 1.41s p=0.933 n=6
Check Time 14.05s (± 0.40%) 14.15s (± 0.19%) +0.10s (+ 0.72%) 14.12s 14.20s p=0.005 n=6
Emit Time 0.00s (± 0.00%) 0.00s (± 0.00%) ~ 0.00s 0.00s p=1.000 n=6
Total Time 18.56s (± 0.36%) 18.68s (± 0.25%) +0.12s (+ 0.64%) 18.62s 18.76s p=0.010 n=6
xstate - node (v18.15.0, x64)
Memory used 513,416k (± 0.00%) 513,441k (± 0.01%) ~ 513,364k 513,501k p=0.173 n=6
Parse Time 3.28s (± 0.26%) 3.28s (± 0.16%) ~ 3.27s 3.28s p=0.533 n=6
Bind Time 1.54s (± 0.26%) 1.54s (± 0.00%) ~ 1.54s 1.54s p=0.405 n=6
Check Time 2.88s (± 1.03%) 2.84s (± 0.68%) -0.04s (- 1.45%) 2.82s 2.87s p=0.037 n=6
Emit Time 0.08s (± 4.99%) 0.07s (± 5.69%) 🟩-0.01s (-12.24%) 0.07s 0.08s p=0.008 n=6
Total Time 7.78s (± 0.38%) 7.73s (± 0.24%) -0.05s (- 0.64%) 7.72s 7.77s p=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)
Benchmark Name Iterations
Current pr 6
Baseline baseline 6

Developer Information:

Download Benchmarks

@typescript-bot
Copy link
Collaborator

Hey @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/@[email protected][email protected]/node_modules/@definitelytyped/dtslint/dist/index.js:194:28)
    at runTests (/home/vsts/work/1/DefinitelyTyped/node_modules/.pnpm/@[email protected][email protected]/node_modules/@definitelytyped/dtslint/dist/index.js:186:20)

You can check the log here.

@typescript-bot
Copy link
Collaborator

@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

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

NWYLZW commented Feb 22, 2024

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

declare const notMatched: unique symbol
function isWhat<Input = unknown, T = never>(
  match: (input: Input, _: typeof notMatched) => T | typeof notMatched
): (
  (x: Input) => x is [T] extends [Input] ? Input & T : never
) {
  return ((x: any): boolean => {
    try {
      return match(x, notMatched) !== notMatched
    } catch (e) {
      if ([notMatched, void 0, null, TypeError].includes(e as any)) {
        return false
      }
      throw e
    }
  }) as any
}

const strs0 = [1, '1', true].filter(
//    ^? string[]
  isWhat((t, _) => typeof t === 'string' ? t : _)
)

const strs1 = [1, '1', true].filter(
//    ^? string[]
  isWhat(t => {
    if (typeof t === 'string') return t
    throw void 0
  })
)

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.

@btoo
Copy link

btoo commented Apr 10, 2024

i'd love to be able to

Handle multiple returns

this would allow me to do stuff like this, or even a satisfies version of that, to handle (with constant runtime performance, à la switch) an arbitrarily large amount of narrowing cases!

Edit: @danvk is working on it over at #58154

@danvk
Copy link
Contributor Author

danvk commented Apr 10, 2024

@btoo adding support for multiple returns isn't that difficult. I have a branch that implements it here: main...danvk:TypeScript:multi-return-predicate

There would be a performance impact since this would run on more functions, and it's unclear whether this would infer enough additional type predicates for that to be worthwhile. (I'd put up a PR but I'm getting a new circularity error in emitter.ts — another downside of running on more functions!) This is up at #58154.

@RDIL
Copy link

RDIL commented Jun 22, 2024

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

@jakebailey
Copy link
Member

Yes, Boolean is not a type predicate.

@ljharb
Copy link
Contributor

ljharb commented Jun 22, 2024

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

@jcalz
Copy link
Contributor

jcalz commented Jun 22, 2024

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
Contributor Author

danvk commented Jun 22, 2024

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

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:

const isTrue = <T>(o: T | undefined | null | false | 0 | ""): o is T => !!o;

const array = [0, "", 5, "hello", false as const, null, undefined]
const result: (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
Contributor Author

danvk commented Jul 10, 2024

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

const isTrue = <T,>(o: T | undefined | null | false | 0 | ""): o is T => !!o;

let x = 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
Copy link

leppaott commented Aug 1, 2024

.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
Contributor

jcalz commented Aug 1, 2024

Yes, we talked about it just a few comments back

@jcalz
Copy link
Contributor

jcalz commented Aug 26, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
For Uncommitted Bug PR for untriaged, rejected, closed or missing bug
Projects
None yet