Skip to content

Assertions in control flow analysis - #32695

Merged
ahejlsberg merged 44 commits into
masterfrom
assertionsInControlFlow
Sep 23, 2019
Merged

Assertions in control flow analysis#32695
ahejlsberg merged 44 commits into
masterfrom
assertionsInControlFlow

Conversation

@ahejlsberg

@ahejlsbergahejlsberg commented Aug 3, 2019

Copy link
Copy Markdown
Member

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 asserts modifier that can be used in type predicates:

declarefunctionassert(value: unknown): asserts value;declarefunctionassertIsArrayOfStrings(obj: unknown): asserts obj is string[];declarefunctionassertNonNull<T>(obj: T): asserts obj is NonNullable<T>;

An asserts return type predicate indicates that the function returns only when the assertion holds and otherwise throws an exception. Specifically, the assert x form indicates that the function returns only when x is truthy, and the assert x is T form indicates that the function returns only when x is of type T. An asserts return type predicate implies that the returned value is of type void, and there is no provision for returning values of other types.

The effects of calls to functions with asserts type predicates are reflected in control flow analysis. For example:

functionf1(x: unknown){assert(typeofx==="string");returnx.length;// x has type string here}functionf2(x: unknown){assertIsArrayOfStrings(x);returnx[0].length;// x has type string[] here}functionf3(x: string|undefined){assertNonNull(x);returnx.length;// x has type string here}

From a control flow analysis perspective, a call to a function with an asserts x return type is equivalent to an if statement that throws when x is falsy. For example, the control flow of f1 above is analyzed equivalently to

functionf1(x: unknown){if(!(typeofx==="string")){throw ...;}returnx.length;// x has type string here}

Similarly, a call to a function with an asserts x is T return type is equivalent to an if statement that throws when a call to a function with an x is T return type returns false. In other words, given

declarefunctionisArrayOfStrings(obj: unknown): obj is string[];

the control flow of f2 above is analyzed equivalently to

functionf2(x: unknown){if(!isArrayOfStrings(x)){throw ...;}returnx[0].length;// x has type string[] here}

Effectively, assertIsArrayOfStrings(x) is just shorthand for assert(isArrayOfStrings(x)).

In addition to support for asserts, we now reflect effects of calls to never-returning functions in control flow analysis.

functionfail(message?: string): never{thrownewError(message);}functionf3(x: string|undefined){if(x===undefined)fail("undefined argument");x.length;// Type narrowed to string}functionf4(x: number): number{if(x>=0)returnx;fail("negative number");}functionf5(x: number): number{if(x>=0)returnx;fail("negative number");x;// Unreachable code error}

Note that f4 is considered to not have an implicit return that contributes undefined to the return value. Without the call to fail an error would have been reported.

A function call is analyzed as an assertion call or never-returning call when

  • the call occurs as a top-level expression statement, and
  • the call specifies a single identifier or a dotted sequence of identifiers for the function name, and
  • each identifier in the function name references an entity with an explicit type, and
  • the function name resolves to a function type with an asserts return type or an explicit never return 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.

@acutmore

acutmore commented Aug 3, 2019

Copy link
Copy Markdown
Contributor

Really like this!

Curious if instead of asserts x it was considered to special case asserts x is true? Might be easier for people to learn/read for the cost of more complexity in the compiler

@ahejlsberg

Copy link
Copy Markdown
MemberAuthor

Curious if instead of asserts x it was considered to special case asserts x is true?

No, because the two are not equivalent. asserts x reflects the full effects of a logical expression when x is truthy, similar to an equivalent if statement. assert x is true simply narrows the type of a variable passed for x, similar to the effects of passing x to an equivalent user defined type predicate function.

@ahejlsberg

Copy link
Copy Markdown
MemberAuthor

@typescript-bot perf test this

@typescript-bot

typescript-bot commented Aug 3, 2019

Copy link
Copy Markdown
Contributor

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!

@dragomirtitian

dragomirtitian commented Aug 3, 2019

Copy link
Copy Markdown
Contributor

@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

Copy link
Copy Markdown
Contributor

Ah! So asserts x declares it’s checking ‘truthy' rather than ‘true’

declare function assert(x): asserts x;
declare const x: string | null;
assert(x);
x.length; // x narrowed to string

@ahejlsberg

Copy link
Copy Markdown
MemberAuthor

Ah! So asserts x declares it’s checking ‘truthy' rather than ‘true’

It's not just that.

asserts x reflects the full effects of the logical expression passed as an argument. E.g. assert(typeof x === "string" || typeof x === "number") narrows x to string | number in the following statements.

assert x is true however only affects a variable passed as an argument, i.e. assertIsTrue(x) narrows x to type true in the following, but does not reflect the effects of a logical expression passed as an argument.

@typescript-bot

Copy link
Copy Markdown
Contributor

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

Here they are:

Comparison Report - master..32695

Metricmaster32695DeltaBestWorst
Angular - node (v12.1.0, x64)
Memory used325,416k (± 0.03%)325,819k (± 0.02%)+403k (+ 0.12%)325,735k326,007k
Parse Time1.44s (± 0.91%)1.43s (± 0.74%)-0.01s (- 0.97%)1.41s1.45s
Bind Time0.76s (± 0.78%)0.77s (± 1.49%)+0.00s (+ 0.52%)0.75s0.81s
Check Time4.22s (± 0.44%)4.27s (± 0.35%)+0.04s (+ 1.07%)4.24s4.32s
Emit Time5.21s (± 0.49%)5.27s (± 0.82%)+0.06s (+ 1.07%)5.21s5.39s
Total Time11.64s (± 0.33%)11.73s (± 0.48%)+0.09s (+ 0.78%)11.65s11.90s
Monaco - node (v12.1.0, x64)
Memory used345,830k (± 0.02%)346,156k (± 0.02%)+326k (+ 0.09%)346,045k346,293k
Parse Time1.19s (± 0.64%)1.17s (± 0.48%)-0.02s (- 1.52%)1.16s1.18s
Bind Time0.67s (± 0.33%)0.68s (± 0.54%)+0.00s (+ 0.75%)0.67s0.68s
Check Time4.28s (± 0.44%)4.29s (± 0.35%)+0.01s (+ 0.28%)4.26s4.32s
Emit Time2.84s (± 0.67%)2.85s (± 0.97%)+0.00s (+ 0.18%)2.81s2.93s
Total Time8.97s (± 0.38%)8.98s (± 0.28%)+0.01s (+ 0.09%)8.94s9.05s
TFS - node (v12.1.0, x64)
Memory used301,335k (± 0.02%)301,644k (± 0.02%)+310k (+ 0.10%)301,532k301,724k
Parse Time0.92s (± 0.84%)0.91s (± 0.92%)-0.02s (- 1.74%)0.89s0.92s
Bind Time0.62s (± 1.08%)0.63s (± 0.80%)+0.01s (+ 1.30%)0.61s0.63s
Check Time3.83s (± 0.46%)3.86s (± 0.54%)+0.03s (+ 0.89%)3.79s3.89s
Emit Time2.94s (± 0.63%)2.96s (± 0.76%)+0.02s (+ 0.75%)2.89s3.01s
Total Time8.30s (± 0.34%)8.34s (± 0.26%)+0.05s (+ 0.57%)8.30s8.38s
Angular - node (v8.9.0, x64)
Memory used343,931k (± 0.01%)344,376k (± 0.01%)+445k (+ 0.13%)344,294k344,499k
Parse Time1.93s (± 0.34%)1.83s (± 0.36%)-0.10s (- 5.12%)1.82s1.85s
Bind Time0.82s (± 0.57%)0.82s (± 0.91%)-0.00s (- 0.37%)0.81s0.84s
Check Time5.07s (± 0.33%)5.10s (± 0.81%)+0.03s (+ 0.61%)5.03s5.19s
Emit Time6.08s (± 0.57%)5.97s (± 1.60%)-0.11s (- 1.88%)5.71s6.12s
Total Time13.90s (± 0.37%)13.72s (± 0.70%)-0.18s (- 1.32%)13.48s13.86s
Monaco - node (v8.9.0, x64)
Memory used363,317k (± 0.01%)363,607k (± 0.01%)+290k (+ 0.08%)363,514k363,675k
Parse Time1.52s (± 0.45%)1.43s (± 0.31%)-0.09s (- 6.10%)1.42s1.44s
Bind Time0.88s (± 0.39%)0.88s (± 1.68%)+0.01s (+ 0.68%)0.86s0.92s
Check Time5.28s (± 0.35%)5.20s (± 1.37%)-0.08s (- 1.46%)5.03s5.30s
Emit Time2.93s (± 0.37%)3.14s (± 6.26%)+0.21s (+ 7.23%)2.91s3.49s
Total Time10.61s (± 0.14%)10.67s (± 1.38%)+0.05s (+ 0.50%)10.49s10.96s
TFS - node (v8.9.0, x64)
Memory used317,282k (± 0.02%)317,510k (± 0.01%)+228k (+ 0.07%)317,443k317,563k
Parse Time1.23s (± 0.61%)1.13s (± 0.33%)-0.10s (- 7.95%)1.13s1.14s
Bind Time0.66s (± 0.74%)0.67s (± 0.67%)+0.00s (+ 0.60%)0.66s0.68s
Check Time4.47s (± 0.57%)4.49s (± 0.42%)+0.02s (+ 0.54%)4.45s4.53s
Emit Time3.05s (± 0.66%)3.19s (± 1.73%)+0.13s (+ 4.35%)3.05s3.26s
Total Time9.42s (± 0.33%)9.48s (± 0.55%)+0.06s (+ 0.66%)9.36s9.58s
Angular - node (v8.9.0, x86)
Memory used194,854k (± 0.02%)195,000k (± 0.02%)+146k (+ 0.08%)194,928k195,056k
Parse Time1.87s (± 0.46%)1.78s (± 0.47%)-0.09s (- 4.76%)1.76s1.80s
Bind Time0.95s (± 0.87%)0.96s (± 0.73%)+0.01s (+ 0.52%)0.94s0.97s
Check Time4.59s (± 0.72%)4.65s (± 0.56%)+0.07s (+ 1.46%)4.61s4.72s
Emit Time5.85s (± 0.87%)5.78s (± 0.83%)-0.07s (- 1.20%)5.67s5.87s
Total Time13.26s (± 0.54%)13.17s (± 0.48%)-0.09s (- 0.65%)13.03s13.33s
Monaco - node (v8.9.0, x86)
Memory used202,921k (± 0.01%)203,068k (± 0.03%)+147k (+ 0.07%)202,941k203,192k
Parse Time1.59s (± 0.56%)1.48s (± 0.56%)-0.10s (- 6.37%)1.47s1.50s
Bind Time0.70s (± 0.52%)0.71s (± 0.56%)+0.01s (+ 0.99%)0.70s0.72s
Check Time4.90s (± 0.44%)4.90s (± 0.74%)+0.00s (+ 0.02%)4.79s4.96s
Emit Time3.19s (± 0.76%)3.18s (± 0.97%)-0.01s (- 0.38%)3.13s3.25s
Total Time10.38s (± 0.30%)10.28s (± 0.45%)-0.11s (- 1.02%)10.14s10.37s
TFS - node (v8.9.0, x86)
Memory used178,237k (± 0.02%)178,315k (± 0.02%)+78k (+ 0.04%)178,185k178,389k
Parse Time1.30s (± 0.65%)1.19s (± 0.93%)-0.11s (- 8.22%)1.17s1.23s
Bind Time0.62s (± 1.42%)0.64s (± 1.20%)+0.01s (+ 1.93%)0.63s0.66s
Check Time4.28s (± 0.70%)4.34s (± 0.92%)+0.06s (+ 1.28%)4.27s4.41s
Emit Time2.88s (± 0.45%)2.84s (± 2.22%)-0.04s (- 1.35%)2.64s2.93s
Total Time9.09s (± 0.41%)9.01s (± 0.81%)-0.08s (- 0.90%)8.84s9.16s
Angular - node (v9.0.0, x64)
Memory used343,623k (± 0.01%)343,948k (± 0.02%)+325k (+ 0.09%)343,706k344,059k
Parse Time1.68s (± 0.49%)1.67s (± 0.44%)-0.01s (- 0.42%)1.66s1.69s
Bind Time0.77s (± 0.84%)0.77s (± 0.68%)+0.00s (+ 0.13%)0.76s0.78s
Check Time4.78s (± 0.51%)4.83s (± 0.57%)+0.05s (+ 0.98%)4.78s4.91s
Emit Time5.69s (± 1.81%)5.48s (± 0.98%)-0.22s (- 3.85%)5.37s5.61s
Total Time12.92s (± 0.87%)12.74s (± 0.30%)-0.18s (- 1.37%)12.68s12.84s
Monaco - node (v9.0.0, x64)
Memory used363,060k (± 0.03%)363,350k (± 0.03%)+290k (+ 0.08%)363,188k363,663k
Parse Time1.29s (± 0.90%)1.27s (± 0.52%)-0.02s (- 1.24%)1.26s1.28s
Bind Time0.85s (± 0.58%)0.86s (± 0.88%)+0.00s (+ 0.23%)0.84s0.88s
Check Time4.91s (± 0.45%)4.91s (± 0.50%)-0.00s (- 0.02%)4.85s4.94s
Emit Time3.36s (± 0.43%)3.37s (± 0.34%)+0.00s (+ 0.12%)3.35s3.40s
Total Time10.41s (± 0.36%)10.40s (± 0.37%)-0.01s (- 0.09%)10.32s10.50s
TFS - node (v9.0.0, x64)
Memory used317,006k (± 0.02%)317,300k (± 0.02%)+293k (+ 0.09%)317,182k317,415k
Parse Time1.02s (± 0.51%)1.01s (± 0.47%)-0.01s (- 1.08%)1.00s1.02s
Bind Time0.61s (± 0.97%)0.61s (± 0.97%)+0.00s (+ 0.16%)0.60s0.63s
Check Time4.34s (± 0.47%)4.39s (± 0.33%)+0.05s (+ 1.13%)4.35s4.42s
Emit Time3.20s (± 0.62%)3.18s (± 0.48%)-0.01s (- 0.44%)3.15s3.21s
Total Time9.18s (± 0.33%)9.20s (± 0.27%)+0.02s (+ 0.20%)9.14s9.25s
Angular - node (v9.0.0, x86)
Memory used194,883k (± 0.02%)195,048k (± 0.04%)+165k (+ 0.08%)194,887k195,228k
Parse Time1.60s (± 0.55%)1.59s (± 0.37%)-0.01s (- 0.88%)1.58s1.60s
Bind Time0.88s (± 0.53%)0.89s (± 1.07%)+0.01s (+ 0.68%)0.87s0.91s
Check Time4.30s (± 0.69%)4.34s (± 0.75%)+0.04s (+ 0.91%)4.29s4.45s
Emit Time5.53s (± 0.99%)5.54s (± 0.71%)+0.01s (+ 0.22%)5.47s5.65s
Total Time12.31s (± 0.59%)12.35s (± 0.46%)+0.04s (+ 0.37%)12.26s12.47s
Monaco - node (v9.0.0, x86)
Memory used202,929k (± 0.02%)203,055k (± 0.03%)+126k (+ 0.06%)202,917k203,166k
Parse Time1.32s (± 0.85%)1.31s (± 0.67%)-0.01s (- 0.45%)1.30s1.34s
Bind Time0.64s (± 0.62%)0.65s (± 0.58%)+0.00s (+ 0.62%)0.64s0.65s
Check Time4.70s (± 0.56%)4.73s (± 0.71%)+0.03s (+ 0.68%)4.67s4.83s
Emit Time3.09s (± 0.28%)3.09s (± 0.56%)-0.00s (- 0.03%)3.04s3.12s
Total Time9.75s (± 0.32%)9.78s (± 0.42%)+0.03s (+ 0.28%)9.70s9.88s
TFS - node (v9.0.0, x86)
Memory used178,233k (± 0.01%)178,312k (± 0.03%)+79k (+ 0.04%)178,185k178,407k
Parse Time1.04s (± 0.72%)1.03s (± 0.48%)-0.01s (- 1.06%)1.01s1.03s
Bind Time0.57s (± 1.19%)0.57s (± 1.19%)+0.00s (+ 0.17%)0.56s0.59s
Check Time4.12s (± 0.51%)4.14s (± 0.60%)+0.02s (+ 0.39%)4.08s4.19s
Emit Time2.79s (± 1.18%)2.78s (± 1.13%)-0.01s (- 0.43%)2.73s2.88s
Total Time8.53s (± 0.53%)8.52s (± 0.50%)-0.00s (- 0.06%)8.44s8.63s
System
Machine Namets-ci-ubuntu
Platformlinux 4.4.0-142-generic
Architecturex64
Available Memory16 GB
Available Memory1 GB
CPUs4 × Intel(R) Core(TM) i7-4770 CPU @ 3.40GHz
Hosts
  • node (v12.1.0, x64)
  • node (v8.9.0, x64)
  • node (v8.9.0, x86)
  • node (v9.0.0, x64)
  • node (v9.0.0, x86)
Scenarios
  • Angular - node (v12.1.0, x64)
  • Angular - node (v8.9.0, x64)
  • Angular - node (v8.9.0, x86)
  • Angular - node (v9.0.0, x64)
  • Angular - node (v9.0.0, x86)
  • Monaco - node (v12.1.0, x64)
  • Monaco - node (v8.9.0, x64)
  • Monaco - node (v8.9.0, x86)
  • Monaco - node (v9.0.0, x64)
  • Monaco - node (v9.0.0, x86)
  • TFS - node (v12.1.0, x64)
  • TFS - node (v8.9.0, x64)
  • TFS - node (v8.9.0, x86)
  • TFS - node (v9.0.0, x64)
  • TFS - node (v9.0.0, x86)
BenchmarkNameIterations
Current3269510
Baselinemaster10

@ahejlsberg

Copy link
Copy Markdown
MemberAuthor

@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

Copy link
Copy Markdown
Contributor

asserts x reflects the full effects of the logical expression passed as an argument.

This I understand :-). What I am doing a poor job of expressing was that in my mind the reflection is a detail of the call site, and theoretically a function that asserts x is true could be completely obviously to this. Though the more I think about this, the more I can see how that would involve a lot of complexity. As it would almost be similar to supporting something like this:

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 asserts x fanclub!

@felixfbecker

felixfbecker commented Aug 3, 2019

Copy link
Copy Markdown
Contributor

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 : never

Assertion functions are really just functions that throw errors in certain cases. A function returning never means it is always throwing. If a function returns never exactly when the input is a string (i.e. always throws when the input is a string), we know that after that call the value must be a string.

This was also suggested and upvoted in the issue: #8655 (comment)
I think if this feature can be expressed with existing syntax and concepts, adding more keywords and concepts to the language should be avoided (or TS will eventually become too complex).

The only thing a conditional never types cannot express is a manual type checking boolean expression:

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

Expected type of value to be string, got number

instead of

Expected false to be true

which is not helpful. Plain assert() should always be avoided.

It also seems like asserts would not work with the popular expect() assertion style (used in Jest):

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 never return types:

functionexpect<T>(value: T): Matcher<T>interfaceMatcher<T>{toBeString(): Textendsstring ? void : never;}

@ajafffajafff left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

what's the difference to isEntityNameExpression?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good catch! No difference, will change to use the existing function.

Comment threadsrc/compiler/binder.ts Outdated
}

function isDottedName(node: Expression): boolean {
return node.kind === SyntaxKind.Identifier || node.kind === SyntaxKind.PropertyAccessExpression && isDottedName((<PropertyAccessExpression>node).expression);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is there a reason not to include this and super in property access expressions?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I think that would be okay, but I'll have to convince myself it can't trigger circularities in control flow analysis.

Comment threadsrc/compiler/parser.ts Outdated
node.assertsModifier = parseExpectedToken(SyntaxKind.AssertsKeyword);
node.parameterName = parseIdentifier();
if (parseOptional(SyntaxKind.IsKeyword)) {
node.type = parseType();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This makes this type of object polymorphic. Could you instead always assign the property and use undefined if there is no type?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Yup


function parseAssertsTypePredicate(): TypeNode {
const node = <TypePredicateNode>createNode(SyntaxKind.TypePredicate);
node.assertsModifier = parseExpectedToken(SyntaxKind.AssertsKeyword);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Agreed


function parseAssertsTypePredicate(): TypeNode {
const node = <TypePredicateNode>createNode(SyntaxKind.TypePredicate);
node.assertsModifier = parseExpectedToken(SyntaxKind.AssertsKeyword);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

It's possible, but for now I'm going to keep it the way it is.

Comment threadsrc/compiler/factory.ts Outdated
}

export function createTypePredicateNode(parameterName: Identifier | ThisTypeNode | string, type: TypeNode) {
export function createTypePredicateNode(assertsModifier: AssertsToken | undefined, parameterName: Identifier | ThisTypeNode | string, type: TypeNode | undefined) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

should this handle JSDoc as well?

@treybrisbane

Copy link
Copy Markdown

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

j-f1 commented Aug 4, 2019

Copy link
Copy Markdown

Another advantage to using the never type instead as suggested above is that it would also add support for calling e.g. process.exit in a conditional to narrow the type.

@kitsonkkitsonk mentioned this pull request Aug 4, 2019
@zen0wu

Copy link
Copy Markdown

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?

@krzkaczor

krzkaczor commented Aug 4, 2019

Copy link
Copy Markdown

@ahejlsberg I have a couple of questions:

  1. Does it work with async assert as well? Ie. a function that depending on a condition resolves/rejects a promise.
  2. Does it work with assertions on this? I wonder if this can help implement linear types (related: Affine types / ownership system #16148). My dummy example:
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

Copy link
Copy Markdown

How to write invariant with it?

@jack-williams

Copy link
Copy Markdown
Collaborator

@treybrisbane Your second example works, but your first does not because this is not supported in an assert predicate (not sure if that is by-design). So you can track mutations, but this only really works for monotonic references.

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

@felixfbecker

The syntax:

assertString(value: unknown): value extends string ? void : never

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

Copy link
Copy Markdown
Contributor

@jack-williams sorry, updated my comment, what I meant was:

assertString<T>(value: T): Textendsstring ? void : never

which 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 never would lead to this conclusion. TypeScript already infers the never type for functions that always throw, and flags unreachable code after the throw statement. One would think that the fact that the function returns never would also make TS flag code after a call of such function (but doesn't atm). Then by using conditional types we can intuitively model assertions.

@alexreardon

alexreardon commented Aug 4, 2019

Copy link
Copy Markdown

This is needed to correctly move tiny-invariant to typescript: alexreardon/tiny-invariant#45. We have not been able to write a correct typescript invariant

We also use invariant heavily for react-beautiful-dnd, so having this style of guard would making moving rbd over to Typescript much easier atlassian/react-beautiful-dnd#982

@ahejlsberg

ahejlsberg commented Aug 5, 2019

Copy link
Copy Markdown
MemberAuthor

@felixfbecker The difference between the two forms

assertString(value: unknown): asserts valueextends string;
assertString<T>(value: T): Textendsstring ? void : never

is that that we cannot necessarily make conclusions about an argument passed for value from a type argument for T. For example, imagine a type argument was explicitly specified for T, or that multiple parameters reference T, or that T is only referenced in a composite type and not as a naked type parameter. In those cases it is not meaningful to make conclusions for value and we would need rules to exclude them. Which ultimately leads you to the current form.

@ahejlsberg

Copy link
Copy Markdown
MemberAuthor

@typescript-bot perf test this again to observe effects of including this.xxx(...) calls in control flow graph.

@Universal-Invariant

Copy link
Copy Markdown

I just started learning typescript and reading the documents from:
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#assertion-functions

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 isString(val: any): val is string {
return typeof val === "string";
}

function assertIsString(val: any): asserts val is string {
if (typeof val !== "string") {
throw new AssertionError("Not a string!");
}
}

The natural thing is to generalize this in some way:

// Generic assertion function
function assert(value: unknown, message: string = "Assertion failed"): asserts value is T {
if (!predicate(value)) {
throw new Error(message);
}
}

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:
assert(msg)

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?

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