Новое правило языка Flow: ограниченная запись

Flow выпускает новое языковое правило, определяющее тип неаннотированной переменной при ее инициализации. Наряду с этими новыми правилами появилось несколько исправлений ошибок надежности, из-за которых уточнения не становились недействительными. Это серьезное семантическое изменение, которое мы внедряем в рамках нашего постепенного перехода к выводу локальных типов.

Это новое поведение в настоящее время стоит за inference_mode=constrain_writes. Он станет режимом по умолчанию в 0.185.0, а режим classic будет удален в 0.186.0.

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

Этот пост является первым из двух в серии, описывающей новую среду набора текста в Flow. В оставшейся части этого поста объясняются изменения в том, как Flow анализирует ваш код. Если вы хотите узнать больше об изменениях здесь и о том, как они способствуют нашему переходу к выводу локальных типов, ждите более глубокого технического погружения в наш новый дизайн среды, которое выйдет в ближайшие несколько недель.

Более строгая типизация для неаннотированных переменных

Тип переменной теперь будет определяться типом ее инициализатора, а не текущим поведением Flow, позволяющим «расширять» тип неаннотированной переменной по всей программе. Возьмем пример:

// @flow
let x = 3;
//... much later
x = 'str';

До этих изменений Flow позволял назначать 'str' на x. Теперь Flow делает вывод, что тип x равен number, потому что он был инициализирован как 3, поэтому вместо этого он выдаст ошибку при втором назначении. Это значительно приближает поведение неаннотированных переменных к поведению аннотированных переменных, что приводит к гораздо более предсказуемому поведению вывода.

Мы внесли это изменение по двум причинам: старое поведение Flow сбивало с толку, а также оно было принципиально несовместимо с нашим стремлением к локальному выводу типов.

Более подробное объяснение новых правил см. в документации, которая охватывает случаи, в том числе переменные, объявленные без инициализаторов, и переменные, инициализированные нулевым значением.

Более точное уточнение инвалидации

В рамках этого изменения мы обнаружили случаи, когда Flow необоснованно сохранял уточнение, которое должно было быть аннулировано. Было два паттерна, которые чаще всего демонстрировали такое поведение:

function foo() {
  let a: number| null;
  bar(); // 2) Function call *before* the refinement
  if (a == null) throw ''; // 1) This is the refinement
  function bar() {
    a.toFixed(); // 3) Runtime crash!
  }
}

Приведенный выше пример проходит через Flow, несмотря на сбой во время выполнения. a уточняется после вызова bar, поэтому во время выполнения вызов toFixed() рухнет! Эти изменения правильно определяют, что a в bar может быть нулевым, поэтому возникают ошибки при вызове toFixed().

let a: number | null = 3;
function assignA(): boolean {
    a = null;
    return true;
}
if (a != null && assignA()) {
    a.toFixed(); // Runtime crash!
}

Поток также проходит в этом примере, несмотря на сбой во время выполнения. Это связано с тем, что Flow не делал недействительными уточнения вызовов функций, которые происходили в защитном выражении оператора if (пока, для и т. д.). С этими изменениями мы теперь ошибаемся при вызове toFixed(), говоря, что a может быть null.

Но мы не просто более строги во всех случаях. Мы также нашли ненужные ошибки, которые были удалены, потому что Flow был слишком строг с недействительным уточнением:

type Obj = {foo: ?(mixed) => void};
let a: Obj = {};
function invalidate(a: Obj) {a.foo = null}
if (a.foo != null) {
  a.foo(invalidate(a)); // No more error!
}

Поток выдаст ошибку при вызове a.foo в этом примере, но в этом нет необходимости. a.foo оценивается перед вызовом invalidate! Мы больше не ошибаемся с новым поведением в этом примере.

Остальные новые ошибки и как их исправить

В рамках этих изменений нам пришлось сделать вывод вокруг петель немного менее точным. На практике очень маловероятно, что вы столкнетесь с такими ошибками, но их стоит задокументировать:

let x: null | number = 42;
if (x!= null) {
  while (x > 3) { // Error, the refinement on x gets invalidated
    x--;
  }
}

Мы гораздо более агрессивно аннулируем уточнения при входе в цикл, если переменная изменяется циклом. Нам пришлось пойти на этот компромисс, чтобы включить другие части LTI. К сожалению, по нашим оценкам, на практике это случается крайне редко.

Обновление собственных репозиториев

Мы также поставляем два codemod в бинарном файле Flow, чтобы помочь вам обновить ваши репозитории. Вы захотите запустить:

$ flow codemod rename-redefinitions --write .
$ flow codemod annotate-declarations --write .

rename-redefinitions разделит переменную, которая используется с несколькими типами, на отдельные переменные, каждая из которых имеет один тип, если их время жизни не перекрывается. Например:

let x = 3;
(x: number);
x = 'str'; // Error in the new mode
(x: string);
----Transforms Into----
let x = 3;
(x: number);
const xStr = 'str';
(xStr: string);

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

annotate-declarations добавит аннотации типа объединения к значениям, которые по-прежнему полагаются на старое «расширяющее» поведение, которое нельзя разделить с помощью rename-redefinitions. Например:

let x = 3;
if (Math.random()) {
  x = 'str'; // Error in the new mode
}
----Transforms Into----
let x: number | string = 3;
if (Math.random()) {
  x = 'str';
}

Этот второй кодмод вставляет только аннотации типов, поэтому не должно быть никаких изменений во время выполнения. Тем не менее, стоит проверить результаты codemod, прежде чем вносить изменения в рабочую среду.