Как правильно обрабатывать переменные let с обратными вызовами в TypeScript? - PullRequest
1 голос
/ 04 мая 2020

Я немного борюсь с одним общим шаблоном JavaScript во время использования TypeScript. Речь идет о:

  1. объявлении некоторой переменной "let" без установки ей какого-либо начального значения
  2. установки этого значения в переменную в некотором обратном вызове
  3. работа с этой переменной после выполнения этого обратного вызова

Вот пример кода:

const wait = (cb: Function) => // just example of a possible callback
  new Promise((resolve) =>
    setTimeout(() => {
      cb();
      resolve();
    }, 1)
  );

async function v1() {
  let a: { bool: boolean };

  await wait(() => { 
    a = { bool: true }; // from sinse `a` isn't empty
  });

  alert(a); // error: Variable 'a' is used before being assigned. ts(2454)

  if (a === undefined) return; // error: Variable 'a' is used ...

  alert(a); // only now it's okay: { bool: true }
}

Как вы можете видеть:

  • TypeScript понимает, что a может не быть инициализированный
  • , но в то же время TS не понимает, что он может быть инициализирован

Хорошо. Что если я просто добавлю некоторую проверку и возможность быть null:

async function v2() {
  let a: { bool: boolean } | null = null;

  await wait(() => { 
    a = { bool: true };
  });

  alert(a); // no error
  alert(a.bool); // error: possibly null

  if (a === undefined || a === null) return;

  alert(a.bool); // error: Property 'bool' does not exist on type 'never' ts(2339)
}

Так что теперь TypeScript знает, что это не ноль. Итак ... это должно быть { bool: boolean }. Но ... TypeScript считает, что это недостижимая ветвь кода, поэтому тип равен never.

Есть ли какое-нибудь простое разумное решение, чтобы убедить TypeScript в правильности кода и правильном типе { bool: boolean }?

Например:

  • Я не могу установить Нулевое начальное значение. Но на самом деле обычно это вряд ли возможно (потому что есть гораздо более сложные типы)
  • Я могу использовать // @tsignore, as MyType, !, игнорировать правила линтера

Эти ^ дон не очень хорошо для меня выглядит :) Я думаю, что скучаю по чему-то важному.

Мой tsconfig:

{
  "compilerOptions": {
    "sourceMap": true,
    "module": "commonjs",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "target": "ES2019",
    "allowJs": false,
    "checkJs": false,
    "strict": true,
    "resolveJsonModule": true,
    "lib": ["ES2019", "DOM"],
    "types": ["jest", "node"],
    "typeRoots": ["./src/types", "./node_modules/@types"],
    "outDir": "./build",
    "baseUrl": "./src/",
    "paths": { "~/*": ["*"] }
  },
  ...
}

1 Ответ

2 голосов
/ 04 мая 2020

Это комбинация нескольких вещей, кусающих вас.


Основная проблема заключается в том, что компилятор не выполняет свой анализ потока управления , проходя по вызовам функций, находя реализацию этой функции и проверяя, находится ли состояние любых закрытых переменных изменилось. Это было бы «правильным» поведением, но компилятору было бы непозволительно дорого с точки зрения времени и памяти по существу моделировать работу каждой программы для отслеживания таких изменений состояния. Вопрос "когда вызывается функция, как должен компилятор предполагать ее побочные эффекты?" трудно ответить, и в GitHub есть обсуждение этого: microsoft / TypeScript # 9998 .

Текущий компилятор предполагает, что вызовы функций имеют никаких побочных эффектов. Таким образом, если a сброшен до вызова wait(), компилятор также будет считать его неустановленным. Это не так в вашем случае, но есть много, много случаев, когда это желаемое поведение. Это компромисс. Поэтому нам нужно найти обходной путь, при котором компилятор обрабатывает a как «возможно установленный» после вызова wait(), не полагаясь на то, что компилятор сделает это за нас.


Ваш второй подход, при котором вы инициализируете a чем-то вроде null или undefined, является многообещающим, но, к сожалению, вы столкнулись с другой проблемой: если вы присваиваете значение переменной типа объединения, анализ потока управления сузит переменную тип. Таким образом, после let a: { bool: boolean } | null = null; компилятор видит a типа null до тех пор, пока он не будет переназначен. См. micrsoft / TypeScript # 8513 для получения дополнительной информации. Опять же, это часто хорошо. В конце концов, если бы мы не пытались обойти другую проблему, вы бы хотели бы , чтобы компилятор думал, что a - это null там.

Обходной путь здесь (, косвенно упомянутый в вышеприведенной проблеме ), вероятно, использует утверждение типа , чтобы сказать компилятору трактовать null не как тип null, а как тип объединения { bool: boolean } | null:

let a = null as { bool: boolean } | null;

Теперь вы можете позвонить wait(), после чего предполагаемый тип a не изменился (из-за # 9998) и остается объединением. Тогда другой код, показанный ниже, функционирует, как и ожидалось:

if (a === undefined || a === null) return;
alert(a.bool); // okay

Так что, да? Он не идеален ни в коем случае, но это лучшее из того, что я могу предложить, учитывая текущее воплощение TypeScript. Во всяком случае, надеюсь, что это помогает; удачи!

Детская площадка ссылка на код

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...