Search code examples
typescriptdiscordundefined

TypeScript(err:2532): object is possibly undefined dispite null/undefined check


I've been writing a Discord-bot in TypeScript and I added a command that retrieves information from an airport. The only thing you have to provide is the ICAO code of the airport(4 character code that identifies the airport). Now: Obviously users might get the ICAO code wrong and maybe give invalid ones. So that's why I have a method that retrieves the airport object from a JSON-File and looks a bit like this(not an exact replica, but just so you get an idea of it):

public getAirport(icao: string): Airport | undefined {
    return arrayOfAllAirports.find(ap => ap.icao === icao);
}

In my command file/class I use this method to obviously retrieve the airport with the given ICAO code. Then I check if its return value is undefined and if so, I return *some error* and that is that. And that looks kind of like this:

let airportMgr = new AirportManager();
    
let airport = airportMgr.getAirport(icao);
if (airport === undefined) {
    return *invalid icao error*;
}

*blablabla*

let exampleString = `ICAO: ${airport.icao} | Name: ${airport.name}`;
                                    ^error here             ^error here

But in two files/classes in which I use the said method together with the said checks I get an error further down, that the object might be undefined. Now comes the even more confusing bit: A few lines lower. I access some property of 'airport' again and it doesn't say anything.

Now I know I could use as Airport when I retrieve the airport or reassign it etc, but I want a proper solution and not cheat typescript in stupid ways. Do any of you have an Idea on how to get hold of this issue?

edit:

here's the picture of where it worked: https://i.sstatic.net/tTZyJ.png


Solution

  • The problem is that you are using let, not const. let can be reassigned which means that every time there was a chance it might have been changed, its type will be broadened to its original type. Something like this:

    // I made it async to demonstrate something below, for your case it is not important
    async function doStuff() {
      // Some icao code
      const icao = 'some icao';
      let airport: Airport | undefined = getAirport(icao);
    
      // Let's start by checking whether the airport is undefined
      if (airport === undefined) {
        return;
      }
    
      // And now let's break down all the scenarios that work
      // and then all the ones that don't
      //
      // The important concept here is called lexical scope,
      // an "environment" in which variables and constants are defined
    
      // 1. current lexical scope - the scope of this function
      //
      // This should work since the value of airport has just been checked
      // and has not had an opportunity to be changed
      airport.name;
    
      // 2. "child" lexical scope - scope created by a function defined within
      // the current lexical scope
      // 
      // This should not work since you might be calling this function
      // some later time, when the value of airport would have possibly changed
      function someHelperFunction() {
        airport.name;
      }
    
      // The same applies to promise resolution values
      Promise.resolve().then(() => {
        // This should not work
        airport.name
      });
    
      // It works with simple awaits
      const anything = await Promise.resolve('something');
      // This will work
      airport.name;
    
      // A TRICK (!!!)
      //
      // You can at this point create a const of type Airport (because now you are sure
      // it is an airport) and use that in your functions.
      //
      // Since it is defined as const, it cannot be changed 
      // and it will stay an airport like forever and ever
      const airportForSure: Airport = airport;
      function someOtherHelperFunction() {
        // WORKS!!!
        airportForSure.name;
      }
    
      Promise.resolve().then(() => {
        // Yesss
        airportForSure.name
      });
    
      // Then when you change the value of airport, you need to check for undefined again
      airport = getAirport(icao);
    
      // This will not work
      airport.name;
    }
    
    type Airport {
      icao: string;
      name: string;
    }
    
    // Just a placeholder for a function
    declare function getAirport(icao: string): Airport | undefined;
    

    Link to TypeScript playground