Новое правило языка 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, прежде чем вносить изменения в рабочую среду.