3

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 ?

3
  • the issue is types are conflicting, even though you did put in checks to make sure baseDirId is a string, but that ensures that value of it is string, meaning the type defined is still either a string or undefined. You can use something like baseDirId as string to explicitly cast it to string, this is possible because you already made sure it will be string in above code and shouldnt break things, else change the type of rstr function's parameter to match baseDirId Commented Apr 18, 2021 at 11:18
  • 1
    I would prefer not to explicitly type cast baseDirId as string. Also in the code I make sure the var is not undefined (line 18) then check the only other possible type string (line 25). Thus I dont understand what you meant by "but that ensures that value of it is string, meaning the type defined is still either a string or undefined." Commented Apr 18, 2021 at 11:50
  • 1
    I assume this is because of setTimeout(() => console.log(baseDirId), 1000); setTimeout(() => baseDirId = undefined, 500);. Or in other words: A callback might be called at a later point in time, and the value might be undefined then. Commented Apr 18, 2021 at 11:55

2 Answers 2

2

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.

Sign up to request clarification or add additional context in comments.

Comments

1

TypeScript does not check types and even change types at runtime, so baseDirId type would always is string | undefined unless you do narrow types or something else for type, so there are many options you can try.

1. Use default

let baseDirId = args.baseDirId || "valid string"

2. Do a conditional value check

if(args.baseDirId){
  let baseDirId = args.baseDirId
  // ...
  // do something you want
}

But you can't do this directly in following snippet, since you used let to declare baseDirId, and then it won't work due to that it can be changed to undefined at any time unless it's declared via const

if(baseDirId){
  const bleharr = someArr.map((val) => rstr(baseDirId))
}

3. Use ! non-null assertion operator

When you're sure it must be exsiting and you don't want to change anything else

const bleharr = someArr.map((val) => rstr(baseDirId!))

2 Comments

Will it possible to narrow down baseDirId type to string ?
@Solaris Sure, you can do that, but since you used let to declare the baseDirId, even if you use do a conditional value check, seems like it won't work due to that it can be changed to undefined at any time

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.