Typescript: переменная, возможно, неопределенная внутри анонимной функции

#javascript #typescript #ecmascript-6 #arrow-functions

Вопрос:

TLDR; Проверка переменной перед ее использованием в анонимной функции по-прежнему предупреждает, что переменная, возможно, не определена

В приведенном ниже примере кода переменная baseDirId проверяется, если она не определена, а затем передается в функцию array.map, но предупреждения TS baseDirId могут быть неопределенными.

// Ссылка на игровую площадку для машинописи

 
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))
 

Есть ли какой-нибудь возможный случай, когда baseDirId это может быть undefined ?

Почему TS не позволяет этого ? Лучший способ сделать это ?

Комментарии:

1. проблема в том, что типы конфликтуют, даже если вы установили проверки, чтобы убедиться, что baseDirId является строкой, но это гарантирует, что его значение является строковым, то есть определенный тип по-прежнему является строкой или неопределенным. Вы можете использовать что-то вроде baseDirId as string явного приведения его к строке, это возможно, потому что вы уже убедились, что это будет строка в приведенном выше коде и не должно ничего нарушать, иначе измените тип rstr параметра функции, чтобы он соответствовал baseDirId

2. Я бы предпочел явно не вводить приведение baseDirId as string . Также в коде я удостоверяюсь, что var отсутствует undefined (строка 18), затем проверяю единственный другой возможный тип string (строка 25). Таким образом, я не понимаю, что вы имели в виду под «но это гарантирует, что его значение является строковым, то есть определенный тип по-прежнему является либо строкой, либо неопределенным».

3. Я предполагаю, что это из-за setTimeout(() => console.log(baseDirId), 1000); setTimeout(() => baseDirId = undefined, 500); . Или другими словами: обратный вызов может быть вызван в более поздний момент времени, и тогда значение может быть неопределенным.

Ответ №1:

Давайте немного изменим код на

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

поэтому вместо .map прямого вызова он может быть запущен в более поздний момент времени. За это время мог быть написан какой-то другой код baseDirId , в который не определено. Поэтому, чтобы правильно определить тип, Typescript должен был бы проверить, что никакой другой код не переопределяет переменную. Это довольно сложная задача (в некоторых случаях это может быть даже невозможно). Кроме того, это становится еще более сложным, если наша внутренняя функция вызывалась в нескольких местах:

 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
 

Поэтому, когда функции обращаются к переменным из внешней области, компилятор Typescript принимает максимально широкий тип. Сужение типа работает только для самого тела функции. Чтобы сузить тип, вам потребуется либо утверждение типа, либо альтернативно скопировать значение в 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
 

Это также работает в вашем случае.

Ответ №2:

TypeScript не проверяет типы и даже не изменяет типы во время выполнения, поэтому baseDirId тип всегда будет string | undefined таким, если вы не сделаете узкие типы или что-то еще для типа, поэтому вы можете попробовать множество вариантов.

1. Используйте значение по умолчанию

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

2. Выполните проверку условного значения

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

Но вы не можете сделать это непосредственно в следующем фрагменте, так как вы раньше let объявляли baseDirId , и тогда он не будет работать из-за того, что его можно изменить undefined в любое время, если он не объявлен через const

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

3. Используйте ! оператор ненулевого утверждения

Когда вы уверены, что это должно быть потрясающе, и вы не хотите ничего менять

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

Комментарии:

1. Можно ли будет сузить тип baseDirId до string ?

2. @Solaris Конечно, вы можете это сделать, но так как вы раньше let объявляли baseDirId , даже если вы используете проверку условного значения, похоже, это не сработает из-за того, что его можно изменить undefined в любое время