Skip to content

checker.ts: explicit boolean type annotation overrides type guard, silencing an error #45594

Closed
@rafasofizada

Description

@rafasofizada

Bug Report

I wouldn't call this a bug, but rather a piece of code inconsistent with "best practices" and prone to cause more confusion in future.

This is a spin-off from issue #45572. I have investigated the line mentioned in @andrewbranch 's comment, and answered my own question in the subsequent reply.

If you indeed recognize it as a bug, please read the "Solution" section and assign the bug to me. Thanks for working on and maintaining such a wonderful project!

🔎 Search Terms

Control flow analysis of aliased conditional expressions
Type guard result saved to variable

🕗 Version & Regression Information

This was introduced in v4.4.0-beta, by pull request #44730 "Control flow analysis of aliased conditional expressions", commit 08b6e2e.

"inlining CFA" through an explicit type annotation:

@@ -38340,9 +38340,9 @@
function checkModuleDeclaration(node: ModuleDeclaration) {
    ...
-   const isAmbientExternalModule = isAmbientModule(node);
+   const isAmbientExternalModule: boolean = isAmbientModule(node);

⏯ Playground Links & 💻 Code

// utilities.ts

export function isAmbientModule(node: Node): node is AmbientModuleDeclaration {
    return isModuleDeclaration(node) && (node.name.kind === SyntaxKind.StringLiteral || isGlobalScopeAugmentation(node));
}

export function isExternalModuleAugmentation(node: Node): node is AmbientModuleDeclaration {
    return isAmbientModule(node) && isModuleAugmentationExternal(node);
}          ^^^^^^^^^^^^^^^^^^^^^

Playground link illustrating the "Before" code example
Before #44730 (v4.3.5):

function checkModuleDeclaration(node: ModuleDeclaration) {
    // ...
    
    // Type guard result gets assigned to a variable.
    const isAmbientExternalModule = isAmbientModule(node);

    // ...

    // Control flow analysis would've narrowed `node` to AmbientModuleDeclaration 
    if (isAmbientExternalModule) {
        // ... so, here, `node` doesn't get narrowed to AmbientModuleDeclaration — it stays just a ModuleDeclaration, 
        // as annotated in the function signature

        if (isExternalModuleAugmentation(node)) {

            // Now, because the type guard is inside the conditional statement, `node` will get narrowed to AmbientModuleDeclaration.

        }
        // However, outside the if statement, `node` is still ModuleDeclaration. No errors.
        // If CFA recognized that isAmbientExternalModule narrows `node` to AmbientModuleDeclaration,
        // the redundancy in isExternalModuleAugmentation() type guard (which contains isAmbientModule() in itself)
        // would cause `node` to be narrowed down to `never`.
        else if ( /* `node` is `ModuleDeclaration` here... */ ) { 
       
        }
        else {
            // ...and here.
        }
    }

    // ...
}

Playground link illustrating the "After, current" code example
After (with explicit type annotation, e.g. current version, WITHOUT error) #44730 (v4.4-beta):

function checkModuleDeclaration(node: ModuleDeclaration) {
    // ...
    
    // Type guard result gets assigned to a variable. CFA would've narrow down `node`
    // in conditional statements below, IF NOT for the `boolean` explicit annotation. Because of it, CFA no longer treats isAmbientExternalModule as a type guard result. No type narrowing will happen below, so further this example
    // is identical to the "Before" example above.
    const isAmbientExternalModule: boolean = isAmbientModule(node);
    
    // ...
}

Playground link illustrating the "After, current" code example
After #44730 (edited out the type annotation, GIVES ERROR) (v4.4-beta):

function checkModuleDeclaration(node: ModuleDeclaration) {
    // ...
    
    // Type guard result gets assigned to a variable. Now, there's no explicit type annotation, 'boolean' doesn't override
    // the type guard's result (predicament?)
    const isAmbientExternalModule = isAmbientModule(node);

    // ...

    // Control flow analysis NARROWS `node` to AmbientModuleDeclaration 
    if (isAmbientExternalModule) {
        // And here, because both isAmbientExternalModule and isExternalModuleAugmentation assert that `node` is AmbientModuleDeclaration ...
        if (isExternalModuleAugmentation(node)) {
            // `node` is `AmbientModuleDeclaration` here 
        }
        // ... CFA will exhaust all possible types for `node` after the first if() statement 
        // (what else can AmbientModuleDeclaration be if not an AmbientModuleDeclaration?)
        // so `node` will be `never`.
        else if ( /* `node` is `never` here... */ ) { 
       
        }
        else {
            // ...and here.
        }
    }

    // ...
}

✅ Solution

Playground link the proposed solution based on prev. examples

src/compiler/checker.ts

function checkModuleDeclaration(node: ModuleDeclaration) {
    ...
    // Removing explicit boolean type annotation will allow CFA to narrow `node` in subsequent conditional statements.
-   const isAmbientExternalModule: boolean = isAmbientModule(node);
+   const isAmbientExternalModule = isAmbientModule(node);

    ...
    
    if (isAmbientExternalModule) {
         // Removing the second isAmbientModule() check (as a part of isExternalModuleAugmentation()) will prevent
         // CFA from narrowing `node` to `never`.
-       if (isExternalModuleAugmentation(node)) {
+       if (isModuleAugmentationExternal(node)) {
        ...

Metadata

Metadata

Assignees

No one assigned

    Labels

    UnactionableThere isn't something we can do with this issueWorking as IntendedThe behavior described is the intended behavior; this is not a bug

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions