Skip to content

Control flow based type narrowing for assert(...) calls #8655

Closed
@yortus

Description

@yortus

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:

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.

Metadata

Metadata

Assignees

Labels

Design LimitationConstraints of the existing architecture prevent this from being fixedFix AvailableA PR has been opened for this issue

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions