-
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 based type guards #6959
Conversation
Do and for statements not implemented yet
Instead of only on Identifiers
# Conflicts: # src/compiler/binder.ts # src/compiler/checker.ts
Use the function |
@@ -1405,7 +1405,7 @@ namespace ts { | |||
} | |||
|
|||
// True if the given identifier, string literal, or number literal is the name of a declaration node | |||
export function isDeclarationName(name: Node): name is Identifier | StringLiteral | LiteralExpression { | |||
export function isDeclarationName(name: Node): name is (Identifier & { __weakTypeGuard: void; }) | StringLiteral | LiteralExpression { |
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.
I don't see this brand applied anywhere - is there a reason for this?
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.
If a type guard returns false, it is expected that the argument is not of the specified type. However, isDeclarationName
can return false when name
is an identifier. This wasn't an issue, as isDeclarationName
was never used in an if statement with an else. However, with control flow based type guards, this wasn't working anymore. The then-block of an if statement contained a return, thus the statements after the if were the else branch of the if. It might not be the best way to solve this, but it's working..
After a bit of sniffing with your code, I think the (or one of the?) test(s) which hangs is |
@@ -6852,81 +6852,135 @@ namespace ts { | |||
} | |||
} | |||
|
|||
interface PreviousOccurency { |
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.
This type isn't used anywhere, AFAIK.
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.
You're right, I forgot to remove it during refactoring.
They caused infinite recursion without this change
# Conflicts: # src/compiler/checker.ts # tests/baselines/reference/assignmentCompatability3.symbols
It took a while, but I think most things are working now. I have updated the PR for the dotted names type guards and this type guards. I think that two minor problems: handling of a try/catch block and The following code compiles, but gives the wrong type in VSCode, with this custom build. When I hover the last let x: string | boolean | number;
if (typeof x === "string") {}
else if (typeof x === "boolean") {}
else x.toExponential; Since most work has been finished, can someone review my code? |
For let x: number | null;
x = 4; // from here x: number
try {
x = 7;
throw new Error();
}
finally {
x.toString(); // <-- should be ok, it would be annoying if x reverts to number|null
} For symbols, which change type inside the block I am unsure whether it's worth the effort. The union of all their types across the block would be correct but it can get hard to reason about, for IMHO little benefit. I think reverting to their initial type would be OK for a first release, it can always be improved later if people have motivating examples of code where it would be nice. |
As discussed in #5168, here's my PR that adds flow based type guards. This is still work in process. The compiler can compile itself, but one of the tests doesn't terminate.
Related issues: #1764, #1769, #2388
Here's a quick overview of the approach I took. During binding, every node gets references to flow markers that preceded the node in the control flow. A flow marker is either a type guard or some node, such as an identifier or a while loop. This represents the control flow graph.
In
checker.ts
, the type of a variable is calculated as the union of the types after the previous occurrences. The type after a node is in case of an assignment narrowed by the assigned type (see below) and otherwise the same as before the node.At an assignment
x = y
, wherey
can be any expression, the following happens. If the type ofx
is a union type, the constituent parts are filtered. The local type ofx
will be the union of all constituent parts to whichy
is assignable. If this yields an empty union type, the initial type ofx
is used instead. Ifx
is not a union type, the local type after the assignment will be the initial type.Since the incoming flow of a
catch
block can come from anywhere in thetry
block, I decided to fall back to the initial type as a precaution. I can relax that rule if desired, for instance by looking at assignments in thetry
block.Todo:
let x: string | number = ""
doesn't narrow tostring
, butx = ""
does.x = ""; for (;x;) { x; }
, narrowing is removed inside the for (because of recursion), butx = ""; for(;;) { x; }
works as expected.~~Questions:
Node
has anid
property (at runtime it isundefined
). Is that correct or a bug? I currently useflowIndex
(which I will remove later on) ingetPreviousOccurencies
, butid
would be better.createFileDiagnostic
, I think the compiler is trying to create an infinite amount of diagnostics.. Does someone have an idea why that's happening?~~