Uh oh!
There was an error while loading. Please reload this page.
Assertions in control flow analysis - #32695
Conversation
Really like this! Curious if instead of |
ahejlsberg
commented
Aug 3, 2019
No, because the two are not equivalent. |
ahejlsberg
commented
Aug 3, 2019
@typescript-bot perf test this |
Heya @ahejlsberg, I've started to run the perf test suite on this PR at fe70a62. You can monitor the build here. It should now contribute to this PR's status checks. Update: The results are in! |
@ahejlsberg Just curios, the official position when multiple such issues were raised was that adding all potential call expressions will grow the CF graph to much and thus it was not really feasible to add this feature. My question is what changed ? Was the reasoning flawed, other performance improvements now make this less of a perf concern, or this is still experimental and could still be axed if performance does meet expectations ? |
acutmore
commented
Aug 3, 2019
Ah! So |
ahejlsberg
commented
Aug 3, 2019
It's not just that.
|
typescript-bot
commented
Aug 3, 2019
@ahejlsberg Here they are:Comparison Report - master..32695
System
Hosts
Scenarios
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
ahejlsberg
commented
Aug 3, 2019
@dragomirtitian What changed? First realizing that the CFA node to AST node ratio is pretty low (about 10% for the compiler itself, for example), and further that we can restrict ourselves to only including top-level expression statement call nodes in the CFA graph. Again, using the compiler itself as an example, this PR only increases the number of CFA nodes by 7.5%. So, overall we're talking less than 1% of additional memory overhead. And execution time overhead is very low when CFA call nodes turn out to not be assertions. The perf test bot numbers appear to confirm this. Less that 0.1% memory overhead and zero execution time overhead. If anything, I would actually have expected more impact. I guess it's sometimes good to question conventional wisdom. Even when it's your own! |
acutmore
commented
Aug 3, 2019
This I understand :-). What I am doing a poor job of expressing was that in my mind the declareconstx: string|number;constisString=typeofx==='string';// isString: (false & x is number) | (true & x is string);if(isString){x;// x: string;}So I retract all I have said, and have fully joined the |
Love this as it would make input validation (even against something like a JSON schema) a lot less clunky! What makes me think though: Have you thought about expressing this with return types instead? assertString<T>(value: T): Textendsstring ? void : neverAssertion functions are really just functions that throw errors in certain cases. A function returning This was also suggested and upvoted in the issue: #8655 (comment) The only thing a conditional assert(typeofx==='string')but I think that is actually a good thing. People should use specialized assertion functions, because they would throw an assertion error like instead of which is not helpful. Plain It also seems like expect(someValue).toBeString()functionexpect<T>(value: T): Matcher<T>interfaceMatcher<T>{toBeString(): asserts ??? isstring;// can't reference value here anymore}while that would work great with functionexpect<T>(value: T): Matcher<T>interfaceMatcher<T>{toBeString(): Textendsstring ? void : never;} |
ajafff
left a comment
There was a problem hiding this comment.
technically it's a breaking change, because the following code no longer parses without error (but what are the odds such code really exists?)
declarefunctionf(asserts: unknown): asserts isstring;| activeLabels!.pop(); | ||
| } | ||
| function isDottedName(node: Expression): boolean { |
There was a problem hiding this comment.
what's the difference to isEntityNameExpression?
There was a problem hiding this comment.
Good catch! No difference, will change to use the existing function.
| } | ||
| function isDottedName(node: Expression): boolean { | ||
| return node.kind === SyntaxKind.Identifier || node.kind === SyntaxKind.PropertyAccessExpression && isDottedName((<PropertyAccessExpression>node).expression); |
There was a problem hiding this comment.
Is there a reason not to include this and super in property access expressions?
There was a problem hiding this comment.
I think that would be okay, but I'll have to convince myself it can't trigger circularities in control flow analysis.
| node.assertsModifier = parseExpectedToken(SyntaxKind.AssertsKeyword); | ||
| node.parameterName = parseIdentifier(); | ||
| if (parseOptional(SyntaxKind.IsKeyword)) { | ||
| node.type = parseType(); |
There was a problem hiding this comment.
This makes this type of object polymorphic. Could you instead always assign the property and use undefined if there is no type?
| function parseAssertsTypePredicate(): TypeNode { | ||
| const node = <TypePredicateNode>createNode(SyntaxKind.TypePredicate); | ||
| node.assertsModifier = parseExpectedToken(SyntaxKind.AssertsKeyword); |
There was a problem hiding this comment.
adding this property here and not assigning it in parseTypeOrTypePredicate where regular TypePredicate nodes are constructed, create yet another hidden class that hinders optimization at runtime.
Either assign it last in this function or (even better) assign it in both functions in the same order
| function parseAssertsTypePredicate(): TypeNode { | ||
| const node = <TypePredicateNode>createNode(SyntaxKind.TypePredicate); | ||
| node.assertsModifier = parseExpectedToken(SyntaxKind.AssertsKeyword); |
There was a problem hiding this comment.
Is there a possibility that there will be more modifiers in the future? If so, would it make sense to put this into Node#modifiers?
There was a problem hiding this comment.
It's possible, but for now I'm going to keep it the way it is.
| } | ||
| export function createTypePredicateNode(parameterName: Identifier | ThisTypeNode | string, type: TypeNode) { | ||
| export function createTypePredicateNode(assertsModifier: AssertsToken | undefined, parameterName: Identifier | ThisTypeNode | string, type: TypeNode | undefined) { |
There was a problem hiding this comment.
this is a breaking API change.
typically there is a new overload added to maintain backwards compatibility. the old signature can be marked as deprecated right away and could be removed later.
| } | ||
| function maybeTypePredicateCall(node: CallExpression) { | ||
| function isDeclarationWithExplicitTypeAnnotation(declaration: Declaration | undefined) { |
There was a problem hiding this comment.
should this handle JSDoc as well?
treybrisbane
commented
Aug 4, 2019
It looks as though this can be used to track mutations, e.g.: classFoo{constructor(publicbar: boolean){}setBar<Textendsboolean>(newBar: T): asserts this is Foo&{bar: T}{this.bar=newBar;}}constFoo=newFoo(false);// foo is Foofoo.setBar(true);// foo is Foo & { bar: true }Or typeFoo={bar: boolean};functionsetBar<Textendsboolean>(foo: Foo,newBar: T): asserts foo is Foo&{bar: T}{foo.bar=newBar;}constfoo: Foo={bar: false};// foo is FoosetBar(foo,true);// foo is Foo & { bar: true }Is this correct? |
j-f1
commented
Aug 4, 2019
Another advantage to using the |
zen0wu
commented
Aug 4, 2019
Really nice! Maybe we could use “asserts false” to represent a function that does not return? (Throws exception) This could help a bunch of case like assertNever, or unimplemented? Or maybe just “assert x is never” works? |
@ahejlsberg I have a couple of questions:
classSocket{publicasyncopen()assertsthis is CloseableSocket{console.log("Opening...")}publicasyncclose()assertsthis is OpenableSocket{console.log("Closing...")}}interfaceCloseableSocket{close()assertsthis is OpenableSocket;}interfaceOpenableSocket{open()assertsthis is CloseableSocket;}Now it would be impossible to call open on the already opened socket and close the already closed socket. This would be really cool to see! |
goodmind
commented
Aug 4, 2019
How to write |
jack-williams
commented
Aug 4, 2019
@treybrisbane Your second example works, but your first does not because @krzkaczor Pre-emptive apology for the pedantry, sorry. What you implement there is known as type-state. Linear (or affine) types are required to soundly implement type-state, but that code does not actually guarantee there is only one reference to a given object. That still looks like an interesting use of this PR though, and if you assume that the user is careful with their aliasing you might be able to add a lot of type-safety. The syntax: also relies on new concepts, specifically having an expression (identifier) appearing in the check-type of a conditional type. On the surface I think it looks familiar to existing ideas, but there may be a non-trivial amount of new concepts required to implement and explain that feature thoroughly. I think if you want meaningful assertion messages (which is definitely desirable), it could be written like: functionassertString(value: unknown): asserts value is string{if(typeofvalue!=="string"){throw"Expected 'string', got ${typeof value}";}} |
felixfbecker
commented
Aug 4, 2019
@jack-williams sorry, updated my comment, what I meant was: assertString<T>(value: T): Textendsstring ? void : neverwhich does not require any new concepts. In fact, I would argue, it is almost a bit unexpected that this does not work already, because the semantics of |
This is needed to correctly move We also use |
@felixfbecker The difference between the two forms assertString(value: unknown): asserts valueextends string;assertString<T>(value: T): Textendsstring ? void : neveris that that we cannot necessarily make conclusions about an argument passed for |
ahejlsberg
commented
Aug 5, 2019
@typescript-bot perf test this again to observe effects of including |
Universal-Invariant
commented
Mar 11, 2025
I just started learning typescript and reading the documents from: It says: "These assertion signatures are very similar to writing type predicate signatures:" and it begs the question if one can combine the assert with the predicate? E.g., given: function assertIsString(val: any): asserts val is string { The natural thing is to generalize this in some way: // Generic assertion function But it would be nice if we didn't have to pass the predicate as an argument but use it's name as a special type: or possibly assertIs(msg) and the compiler does a few checks to determine if T has a predict with the same name and starting with "is" "backing it" and then uses that. This would allow one to have to leverage using both guard predicates and assertion guards together in a convenient way. Maybe there is already a way to achieve this? |
With this PR we reflect the effects of calls to
assert(...)functions and never-returning functions in control flow analysis. We also improve analysis of the effects of exhaustive switch statements, and report unreachable code errors for statements that follow calls to never-returning functions or exhaustive switch statements that return or throw in all cases.The PR introduces a new
assertsmodifier that can be used in type predicates:An
assertsreturn type predicate indicates that the function returns only when the assertion holds and otherwise throws an exception. Specifically, theassert xform indicates that the function returns only whenxis truthy, and theassert x is Tform indicates that the function returns only whenxis of typeT. Anassertsreturn type predicate implies that the returned value is of typevoid, and there is no provision for returning values of other types.The effects of calls to functions with
assertstype predicates are reflected in control flow analysis. For example:From a control flow analysis perspective, a call to a function with an
asserts xreturn type is equivalent to anifstatement that throws whenxis falsy. For example, the control flow off1above is analyzed equivalently toSimilarly, a call to a function with an
asserts x is Treturn type is equivalent to anifstatement that throws when a call to a function with anx is Treturn type returns false. In other words, giventhe control flow of
f2above is analyzed equivalently toEffectively,
assertIsArrayOfStrings(x)is just shorthand forassert(isArrayOfStrings(x)).In addition to support for
asserts, we now reflect effects of calls to never-returning functions in control flow analysis.Note that
f4is considered to not have an implicit return that contributesundefinedto the return value. Without the call tofailan error would have been reported.A function call is analyzed as an assertion call or never-returning call when
assertsreturn type or an explicitneverreturn type annotation.An entity is considered to have an explicit type when it is declared as a function, method, class or namespace, or as a variable, parameter or property with an explicit type annotation. (This particular rule exists so that control flow analysis of potential assertion calls doesn't circularly trigger further analysis.)
EDIT: Updated to include effects of calls to never-returning functions.
Fixes#8655.
Fixes#11572.
Fixes#12668.
Fixes#13241.
Fixes#18362.
Fixes#20409.
Fixes#20823.
Fixes#22470.
Fixes#27909.
Fixes#27388.
Fixes#30000.