Search code examples
javascripttypescriptecmascript-6arrow-functions

Typescript: variable possibly undefined inside anonymous function


TLDR; Checking variable before using it in a anonymous function still TS warns variable possibly undefined

In the below code example variable baseDirId is checked if undefined then passed to array.map function but TS warns baseDirId can be undefined.

// Typescript Playground link


const rstr = async (a: string) => {
  return a + "bleh"
}

const args: { baseDirId: string | undefined } = {
  baseDirId: "someid"
  // baseDirId: undefined
}

const someArr = ["bleh1", "bleh2"]

const func1 = async (): Promise<void> => {
  try {
    // Assume baseDirId can be undefined
    let baseDirId = args.baseDirId

    // Trigger if baseDirId is undefined
    if (!baseDirId) {
      const baseDirObj = { id: "valid string" }
      baseDirId = baseDirObj.id
    }
    console.log(typeof baseDirId)

    // baseDirId cant be anything other than a string 
    if (typeof baseDirId !== "string") {
      return Promise.reject("Base Dir response invalid")
    }

    // Why is baseDirId `string | undefined` inside rstr call below even after above checks
    const bleharr = someArr.map((val) => rstr(baseDirId))
    console.log(await Promise.all(bleharr))
  } catch (err) {
    console.error(err)
  }
}

func1().catch(err => console.error(err))

Is there any possible case where baseDirId can be undefined ?

Why wont TS allow it ? Better way to do it ?


Solution

  • Let's slightly change the code to

     return () => someArr.map((val) => rstr(baseDirId))
    

    so instead of calling .map directly it might get run at a later point in time. Some other code might've written undefined into baseDirId in the meantime. Therefore to correctly infer the type, Typescript would've to check that no other code somewhen overrides the variable. That's quite a complicated task (it could even be impossible in some corner cases). Also this gets even more complicated if our inner function was called at multiple places:

    let mutable: string | undefined = /*...*/;
    const inner = () => fn(mutable); // << how to infer this?
    
    mightCall(inner); // if called back here, mutable would be "string | undefined"
    if(typeof mutable === "string") mightCall(inner); // if called here, mutable could be narrowed down to "string", but only if mightCall calls back synchronously
    
    mutable = undefined; // if mightCall calls back asynchronously, 'mutable' would have to be inferred as undefined
    

    Therefore when functions access variables from the outer scope, the Typescript compiler assumes the widest possible type. Type narrowing only works for the function's body itself. To narrow down the type you'd either need a type assertion, or alternatively copy the value into a const:

     let mutable: string | undefined = /*...*/;
     if(typeof mutable !== "string") return;
     // mutable get's narrowed down to string here due to the typeof type guard
     const immutable = mutable;
      //        ^ inferred as string, this is the widest possible type
    

    This also works in your case.