| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Sorry, something went wrong.
|
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 |
Sorry, something went wrong.
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. |
Sorry, something went wrong.
|
TypeScript Bot (@typescript-bot) perf test this |
Sorry, something went wrong.
|
Heya Anders Hejlsberg (@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! |
Sorry, something went wrong.
|
Anders Hejlsberg (@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 ? |
Sorry, something went wrong.
|
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 |
Sorry, something went wrong.
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. |
Sorry, something went wrong.
|
Anders Hejlsberg (@ahejlsberg) Comparison Report - master..32695
System
Hosts
Scenarios
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Sorry, something went wrong.
|
Titian Cernicova-Dragomir (@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! |
Sorry, something went wrong.
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: declare const x: string | number;
const isString = typeof x === '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! |
Sorry, something went wrong.
|
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): T extends string ? void : neverAssertion 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) The only thing a conditional never types cannot express is a manual type checking boolean expression: assert(typeof x === '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()
function expect<T>(value: T): Matcher<T>
interface Matcher<T> {
toBeString(): asserts ??? is string; // can't reference value here anymore
}while that would work great with never return types: function expect<T>(value: T): Matcher<T>
interface Matcher<T> {
toBeString(): T extends string ? void : never;
} |
Sorry, something went wrong.
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?)
declare function f(asserts: unknown): asserts is string;
Sorry, something went wrong.
| activeLabels!.pop(); | ||
| } | ||
|
|
||
| function isDottedName(node: Expression): boolean { |
There was a problem hiding this comment.
what's the difference to isEntityNameExpression?
Sorry, something went wrong.
There was a problem hiding this comment.
Good catch! No difference, will change to use the existing function.
Sorry, something went wrong.
| } | ||
|
|
||
| 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?
Sorry, something went wrong.
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.
Sorry, something went wrong.
| 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?
Sorry, something went wrong.
There was a problem hiding this comment.
Yup
Sorry, something went wrong.
|
|
||
| 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
Sorry, something went wrong.
There was a problem hiding this comment.
Agreed
Sorry, something went wrong.
|
|
||
| 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?
Sorry, something went wrong.
There was a problem hiding this comment.
It's possible, but for now I'm going to keep it the way it is.
Sorry, something went wrong.
| } | ||
|
|
||
| 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.
Sorry, something went wrong.
| } | ||
|
|
||
| function maybeTypePredicateCall(node: CallExpression) { | ||
| function isDeclarationWithExplicitTypeAnnotation(declaration: Declaration | undefined) { |
There was a problem hiding this comment.
should this handle JSDoc as well?
Sorry, something went wrong.
|
It looks as though this can be used to track mutations, e.g.: class Foo {
constructor(public bar: boolean) {}
setBar<T extends boolean>(newBar: T): asserts this is Foo & { bar: T } {
this.bar = newBar;
}
}
const Foo = new Foo(false);
// foo is Foo
foo.setBar(true);
// foo is Foo & { bar: true }Or type Foo = { bar: boolean };
function setBar<T extends boolean>(foo: Foo, newBar: T): asserts foo is Foo & { bar: T } {
foo.bar = newBar;
}
const foo: Foo = { bar: false };
// foo is Foo
setBar(foo, true);
// foo is Foo & { bar: true }Is this correct? |
Sorry, something went wrong.
|
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. |
Sorry, something went wrong.
|
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? |
Sorry, something went wrong.
|
Anders Hejlsberg (@ahejlsberg) I have a couple of questions:
class Socket {
public async open() asserts this is CloseableSocket {
console.log("Opening...")
}
public async close() asserts this is OpenableSocket {
console.log("Closing...")
}
}
interface CloseableSocket{
close() asserts this is OpenableSocket;
}
interface OpenableSocket{
open() asserts this 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! |
Sorry, something went wrong.
|
How to write invariant with it? |
Sorry, something went wrong.
|
Trey Brisbane (@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. Kris Kaczor (@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: 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: function assertString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw "Expected 'string', got ${typeof value}";
}
} |
Sorry, something went wrong.
|
Jack Williams (@jack-williams) sorry, updated my comment, what I meant was: assertString<T>(value: T): T extends string ? 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 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. |
Sorry, something went wrong.
|
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 |
Sorry, something went wrong.
|
Felix Becker (@felixfbecker) The difference between the two forms assertString(value: unknown): asserts value extends string;assertString<T>(value: T): T extends string ? void : neveris 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. |
Sorry, something went wrong.
|
TypeScript Bot (@typescript-bot) perf test this again to observe effects of including this.xxx(...) calls in control flow graph. |
Sorry, something went wrong.
|
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? |
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
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:
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:
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
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
the control flow of f2 above is analyzed equivalently to
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.
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
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.