-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Control flow analysis of aliased conditional expressions and discriminants #44730
Conversation
@typescript-bot test this |
Heya @ahejlsberg, I've started to run the parallelized community code test suite on this PR at 0bed057. You can monitor the build here. |
Heya @ahejlsberg, I've started to run the perf test suite on this PR at 0bed057. You can monitor the build here. Update: The results are in! |
Heya @ahejlsberg, I've started to run the parallelized Definitely Typed test suite on this PR at 0bed057. You can monitor the build here. |
Heya @ahejlsberg, I've started to run the extended test suite on this PR at 0bed057. You can monitor the build here. |
Cooooooooooooooool!!!!!! Great work!!!!! |
@ahejlsberg Here they are:Comparison Report - main..44730
System
Hosts
Scenarios
Developer Information: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implementation-wise, this looks OK (is there a reason for specifically limiting the nesting analysis to two levels?); but I think the new tests have been elided, since you have a bunch of examples in the OP, but there's no new tests showing the functionality. (I'm interested in some with readonly props, both static and instance, since those don't have examples in your OP, but are referenced in some comments, even though later there's a check for exactly VariableDeclaration
s)
@@ -23553,6 +23581,17 @@ namespace ts { | |||
break; | |||
case SyntaxKind.CommaToken: | |||
return narrowType(type, expr.right, assumeTrue); | |||
// Ordinarily we won't see && and || expressions in control flow analysis because the Binder breaks those |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about ternary expressions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We narrow within the branches of ternary expressions, but not based on the result of ternary expressions, so I don't think there is anything to do there.
I'd like to have a fixed limit so we can avoid maintaining a stack of inlined symbols that would otherwise be needed (to catch accidentally recursive definitions). Two levels is rather arbitrary, could make it three or even five.
Yeah, haven't added tests yet. |
@typescript-bot pack this |
Heya @RyanCavanaugh, I've started to run the tarball bundle task on this PR at 0bed057. You can monitor the build here. |
The |
Hey @RyanCavanaugh, I've packed this into an installable tgz. You can install it for testing by referencing it in your
and then running There is also a playground for this build and an npm module you can use via |
Is it possible to also add narrowing for constants from same destructurization? function test(data: { type: "num", value: number } | { type: "str", value: string }) {
const { type, value } = data;
if (type === "num") {
value.toFixed(); // currently error
data.value.toFixed(); // ok
} else {
value.charAt(0); // currently error
data.value.charAt(0); // ok
}
} UPD. As mentioned in edit I've missed - maybe later, but not in this PR. |
@ahejlsberg, would that be possible to extend this for classes as well, e.g.: class T {
s = "";
}
function f1(x: unknown) {
const isT = x instanceof T;
if (isT) {
x.s; // Ok
}
} Also, maybe to include some other type checks, e.g., |
|
Very cool! Interestingly, I can use |
Should I take this mean narrowing won't work on non- |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would still be nice to have tests documenting our behavior for other const-ish variable types (eg, static read-only fields) which alias a guard, if only to say we don't currently support it.
Correct. For example, the following fails if function test(obj: { readonly data: string | number }) {
const isString = typeof obj.data === 'string';
if (isString) {
let s: string = obj.data;
}
} The rationale here is that we can't know for certain that It does of course work if you copy the mutable property into a function test(obj: { data: string | number }) {
const { data } = obj;
const isString = typeof data === 'string';
if (isString) {
let s: string = data;
}
} |
Cross-linking to #30581. |
Thanks @jcalz ! Correlated unions sound cool! @ahejlsberg would this be the correct issue to watch for this feature of "coupling" between destructured properties / elements? |
@ahejlsberg That would be to prevent that kind of weird behaviour ! |
case SyntaxKind.AmpersandAmpersandToken: | ||
return assumeTrue ? | ||
narrowType(narrowType(type, expr.left, /*assumeTrue*/ true), expr.right, /*assumeTrue*/ true) : | ||
getUnionType([narrowType(type, expr.left, /*assumeTrue*/ false), narrowType(type, expr.right, /*assumeTrue*/ false)]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When assumeTrue
is false, theoretically expr.right
is reachable if and only if expr.left
is truthy?
-getUnionType([narrowType(type, expr.left, /*assumeTrue*/ false), narrowType(type, expr.right, /*assumeTrue*/ false)])
+getUnionType([narrowType(type, expr.left, /*assumeTrue*/ false), narrowType(narrowType(type, expr.left, /*assumeTrue*/ true), expr.right, /*assumeTrue*/ false)])
With this PR we support control flow analysis of conditional expressions and discriminant property accesses indirectly referenced through
const
variables. For example:Intuitively, indirect references to conditional expressions or discriminant property accesses behave exactly as if the expressions were written in-line.
Narrowing through indirect references occurs only when the conditional expression or discriminant property access is declared in a
const
variable declaration with no type annotation, and the reference being narrowed is aconst
variable, areadonly
property, or a parameter for which there are no assignments in the function body.Some examples where narrowing does not occur (but it would had the indirectly referenced conditions been written in-line):
Up to five levels of indirection are analyzed in conditional expressions. For example, two levels of indirections are analyzed in the following code:
It is possible to mix indirect conditional expressions and directly specified conditions:
This PR fixes most of the long list of issues that were closed as duplicates of #12184, but not all. In particular, the pattern of destructuring a discriminant property and a payload property into two local variables and expecting a coupling between the two is not supported as the control flow analyzer doesn't "see" the connection. For example:
We may be able to support that pattern later, but likely not in this PR.
Fixes #12184.
Fixes #19421.
Fixes #19943.
Fixes #24865.
Fixes #26804.
Fixes #31037.
Fixes #31059.
Fixes #31291.
Fixes #31344.
Fixes #31870.
Fixes #33192.
Fixes #34535.
Fixes #35603.
Fixes #36510.
Fixes #37638.
Fixes #37855.
Fixes #39657.
Fixes #39996.
Fixes #40201.
Fixes #40341.
Fixes #41266.
Fixes #41870.
Fixes #43333.