typescriptimmutable.js

ts(2532) Object possibly undefined after undefined check


After upgrading our Typescript and Immutable libraries, I am seeing the error Object is possibly 'undefined'.ts(2532) in many places across our codebase even when there is an undefined check prior to the variable being used.

As a specific example:

export interface AppFlags {
    enabled: string | boolean;
}

const applicationFlags = OrderedMap<string, AppFlags>(/* some data */);


const showPriceWarning: boolean =
    applicationFlags.get("foo") &&
    (applicationFlags.get("foo").enabled as boolean); // undefined warning

console.log(showPriceWarning);

TS Version: 3.7.5

Immutable Version: 4.3.0

Playground Link: https://www.typescriptlang.org/play?ts=3.7.5#code/JYWwDg9gTgLgBAbzgeSgEwKZQ2gsgQzDgF84AzKCEOAIlBAFcZ8AjAGwxoG4AoHjAB6RYcYADsYWMvgDGGOAEEwYAGJt8AcwDOiHnH1wMY1hzQAuOFphRxGuAB84LCBA74xvYnxkQxVuIRgbMAy+DDAvmqaOgC8KOhYOARgADxWNmIaADSKylHaAHwAFACUvN6+-loAFhAA7gAKNnIA6vhQYrYWzq4Y7nAxegaBwaHhkeraAHQaGDBFNGQuNCVwAGRrcEUjIWERYvlaM3MLSxArU0YmOAE6PW5iZXzAZFs7Y-uHx-OLyyWrCCG+h8fl6UzYEA022Uoz2E2i31Of0uxnYOBKPC8PBBWjBEKhNXqTRCGDaHVsT30QA

Before upgrading this worked fine, and it's still functionally correct (it works in runtime), Typescript is just not recognizing it as defined. I know I can fix this with optional chaining (i.e. applicationFlags.get("foo")?.enabled) but that would require a large amount of time to fix all instances of this across the project, and I would like to avoid it if possible.

I'm also seeing this in instances with more traditional if structures, i.e.

export interface AppFlags {
    enabled: string | boolean;
}

const applicationFlags = OrderedMap<string, AppFlags>();

if (applicationFlags.get("foo")) {
    console.log(applicationFlags.get("foo").enabled)
}

Is there a way to fix this across the project? Is this some new intentional behavior? What changed to make this error?


Solution

  • First, let's be clear that the compiler error is intentional. The problem with expressions of the form f() && f().x is that the TypeScript compiler cannot generally know that both calls to f() return the same value, so even if the first result is an object, the second return value could be undefined.

    The typical way to work around this is to put the return value in a separate variable (which also has a small performance benefit), for example:

    const fooFlag = applicationFlags.get("foo");
    
    const showPriceWarning: boolean = fooFlag != null && (fooFlag.enabled as boolean);
    
    if (fooFlag) {
        console.log(fooFlag.enabled);
    }
    

    But this didn't address your main question, which wasn't “How do I fix this?” but rather “Why is this not allowed now when it worked before?” I can't answer that with certainty, but my guess is that you previously didn't have type annotations for OrderedMap installed, so the compiler defaulted to any and didn't do any actual type checking here.

    The lazy fix would be to declare OrderedMap as any:

    const applicationFlags: any = OrderedMap<string, AppFlags>();
    

    But of course that's just disabling type checking again. A better solution is to refactor the broken code following the above example.