Skip to content

Custom type guard / generic predicate leaks outside its scope in certain settingsΒ #43719

Description

Bug Report

πŸ”Ž Search Terms

generic predicate custom type guard

πŸ•— Version & Regression Information

Occurs in versions 4.2, 4.3 and nightly

In version 4.1 the custom type guard isNumClass didn't work at all inside ComplexStore.get

⏯ Playground Link (full example)

Playground link with full code

πŸ’» Code (partial)

Small code sample (please see the playground link for full example).

// This is only a chunk of the problem case, please see the playground link for full code

const isNumClass = <Item extends NumClass<number> | StrClass<string>> (
        item: Item
    ): item is Extract<Item, NumClass<any>> => {
        return (item instanceof NumClass);
    }

/**
 * A an example with 2-dimensional dictionary.
 * 
 * In v4.1 the `isNumClass` type guard doesn't work at all.
 * In v4.2 or later, `isNumClass` type guard leaks outside its
 * scope.
 */
class ComplexStore<Slices extends { [index: string]: Slice }> {
    private slices = { } as Slices;

    public get<SliceId extends keyof Slices, SliceKey extends keyof Slices[SliceId]>(
        sliceId: SliceId, sliceKey: SliceKey
    ): Slices[SliceId][SliceKey] {
        let item = this.slices[sliceId][sliceKey];

        if (isNumClass(item)) {
            item.numExclusive(); // works only since version 4.2
        }

        // unfortunately, doesn't work completely.
        // it seems like item's predicated type leaks outside the bracket...
        
        return item; // type is Extract ...
    }

    public get2<SliceId extends keyof Slices, SliceKey extends keyof Slices[SliceId]>(
        sliceId: SliceId, sliceKey: SliceKey
    ): Slices[SliceId][SliceKey] {
        let item = this.slices[sliceId][sliceKey];

        if (isNumClass(item)) {
            return item;
        }
        // it seems like the compiler asumes the above condition is always
        // truthy

        return item; // type is never
    }
}

πŸ™ Actual behavior

  • The type predicate modifies the type outside its scope. This happens only in a more complex scenario when using generics in a class with a 2-dimensional dictionary. The problem does not occur in the same class with a one-dimensional dictionary.

πŸ™‚ Expected behavior

  • The narrowed type should not leak outside the scope. It should remain unchanged.

I'm sorry if it's not a bug, but I think there are high chances it is. The one-dimensional example works ok now, and worked on in the previous version. The 2-dimensional example didn't work at all prior 4.2 and now works, but there are issues with it

By the way, I'm aware I could use bare instance of, it appeared to work in this simplified example, but it didn't work in my real case (although I will continue to experiment with it).

Activity

  1. changed the title [-]Custom type guard / generic predicate leaks outside the bracket in certain settings[/-] [+]Custom type guard / generic predicate leaks outside its scope in certain settings[/+] on Apr 17, 2021
  2. mckravchyk commented on Apr 19, 2021

    @mckravchyk
    Author

    Update: I mentioned earlier that the default instance of type guard worked in the playground but didn't work in my code. It turns out it was because the issue with instance of appears to be fixed in v4.3 So that's what I'm going to use, as I only had the custom type guards to get around instance of not working.

    Nevertheless, the bug in question is still there.

    To summarize this update:

    if (item instance of NumClass) {
    // works perfectly in 4.3-beta in the complex example with 2-dimensional generic
    // dictionary. (didn't work at all in prior versions).
    }
    
    if (isNumClass(item)) { // custom type guard function
    // works partially in 4.2 and 4.3, with the issue that the type leaks outside
    // the block (this is what the bug report is about)
    // the issue only appears to occur in the complex example where there is 
    // a generic that is a value of a 2-dimensional dictionary
    }
    
    

    Edit:
    I didn't realize that instance of loses the generic (e.g. it narrows to NumClass<number>, rather than to NumClass<T> ). So the custom-type guard is still preferable.

    Edit2:

    A workaround like this seems to work:

    let item = this.slices[sliceId][sliceKey];
    let itemAsNumClass = item;
    
    if (isNumClass(itemAsNumClass)) {
      itemAsNumClass.numExclusive();
    }
    
    // now thanks to the variable alias, item's type is unaffected
    
  3. weswigham commented on Apr 21, 2021

    @weswigham
    Member

    Using the variable (eg, adding an item.get() after the if ends), it looks like it's just the quickinfo that's wrong - our analysis proceeds as though it's still the base type (Slices[SliceId][SliceKey]), like we'd expect.

  4. added
    BugA bug in TypeScript
    and removed
    Needs InvestigationThis issue needs a team member to investigate its status.
    on Apr 21, 2021
  5. weswigham commented on Apr 21, 2021

    @weswigham
    Member

    Correction: Constraint locations (calls, accesses) perform correct analysis. Looks like we're incorrectly calculating the assignability of the nested indexes in the false branch of the flow.

  6. locked as resolved and limited conversation to collaborators on Oct 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

BugA bug in TypeScriptDomain: Indexed Access TypesThe issue relates to accessing subtypes via index accessDomain: check: Control FlowThe issue relates to control flow analysisFix AvailableA PR has been opened for this issue

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions