Description
Now that TypeScript does control flow based type analysis, and there is a never
type in the works, is it possible to consider providing better type checking around assert(...)
function calls that assert that a variable has a certain type at runtime?
TL;DR: some assert
functions are really just type guards that signal via return
/throw
rather than true
/false
. Example:
assert(typeof s === "string"); // throws if s is not a string
s.length; // s is narrowed to string here...
Problem
Asserts are common in contract-based programming, but I've also been coming across this scenario regularly whilst traversing JavaScript ASTs based on the Parser API (I'm using babel to produce/traverse ASTs).
For example, consider the MemberExpression
:
Note we can assume property
is an Identifier
if computed===false
. This is what I'd like to write:
function foo(expr: MemberExprssion) {
if (expr.computed) {
// handle computed case
}
else {
// since computed===false, we know property must be an Identifier
assert(isIdentifier(expr.property));
let name = expr.property.name; // ERROR: name doesn't exist on Identifier|Expression
}
}
Unfortunately that doesn't compile, because expr.property
does not get narrowed after the assert(...)
call.
To get the full benefit of control flow analysis currently, you have to expand the assert call inline:
...
else {
if (!isIdentifier(expr.property)) {
throw new AssertionError(`Expected property to be an Identifier`);
}
let name = expr.property.name; // OK
}
...
While preparing the typings for babel-core
, babel-types
and friends, I noticed that using asserts this way is the norm. babel-types
actually provides an assertXXX
method for every isXXX
method. These assertXXX
functions are really just type guards that signal via return
/throw
rather than true
/false
.
Possible Solutions?
Not sure if it's feasible at all! But the new work on never
in #8652 suggests a few possibilities.
Specific assertions: assertIsT(...)
// normal type guard
function isIdentifier(n: Node): n is Identifier {
return n.type === 'Identifier';
}
// PROPOSED SYNTAX: assert type guard
function assertIdentifier(n: Node): n is Identifier | never {
if (n.type !== 'Identifier') {
throw new AssertionError(`Expected an Identifier`);
}
}
The compiler would reason that if this assert call returns at all, then it can safely narrow the variable type in following code.
General assertions used with type guards: assert(isT(...))
The more general assert(cond: boolean)
function would need a different approach and might not be feasible, but here's an idea:
// General case
declare function assert(cond: boolean): void;
// PROPOSED SYNTAX: Special overload for expressions of the form assert(isT(x))
declare function assert<T>(guard: guard is T): void | never;
For that second assert
overload to work, the compiler on seeing assert(isT(x))
would have to somehow forward the x is T
narrowing from the isT(x)
expression to the assert(...)
expression at compile-time.
Would be great if it also detected/handled things like assert(typeof x == 'string')
.
Not sure if any of this would meet the cost/benefit bar, but it's just an idea.