Улучшенная обработка универсальных типов в Flow

tl; dr: Flow улучшил обработку универсальных типов, запретив ранее разрешенное небезопасное поведение и прояснив сообщения об ошибках.

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

Ограничение экранированных дженериков

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

let external_var = 42;
function f<T>(x: T, should_escape: bool): T | number {
  if (should_escape) {
    external_var = x;
  }
  return external_var;
}

Поскольку он не аннотирован, Flow определит тип external_var и обнаружит, что это T | number, но на самом деле это не тот тип, который имеет смысл иметь: T не существует в области, в которой он определен! Мы называем это T экранированием его области действия. Ранее это было разрешено Flow, но это могло приводить к появлению запутанных сообщений об ошибках, было неясно, что значит для переменной иметь универсальный тип, когда универсальный тип не входит в область видимости, и это блокировало текущую работу по улучшению Flow. родовая система в целом. Другие языки также возражают против универсальных экранирований: например, TypeScript предупреждает, что тип external_var не может быть определен, и вместо этого использует any.

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

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

this типы также являются универсальными

Важно отметить, что все описанное здесь также применимо к типу this при использовании внутри определения класса. Flow моделирует this-тип как универсальный, верхняя граница которого является его охватывающим классом, и не зря: универсальный можно рассматривать как диапазон типов от empty до его верхней границы (которая равна mixed, если не указано иное), и поскольку классы могут быть расширены, тип this также представляет диапазон типов, которые включают все возможные подклассы включающего класса.

Подробный пример

Раньше Flow позволял экранирование без ошибок, но когда экранированный универсальный шаблон используется (как в return external_var выше), это часто приводило к сбивающей с толку ошибке:

6:   return external_var;
            ^ Cannot return `external_var` because `T` [1] is 
              incompatible with `T` [2]. [incompatible-return]
References:
2: function f<T>(x: T, should_escape: bool): T | number {
                    ^ [1]
2: function f<T>(x: T, should_escape: bool): T | number {
                                             ^ [2]

Это сбивает с толку, потому что как T может быть несовместимым с самим собой? С другой стороны, Flow должен выдать здесь некоторую ошибку, потому что в противном случае экранированный универсальный тип мог бы использоваться для нарушения правильности типа без того, чтобы программист осознал это [пример]:

...
f<string>("hello world", true); 
// now external_var is set to "hello world"
var not_a_boolean: boolean | number = f<boolean>(false, false); 
// we now have a string masquerading as a boolean | number!

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

Изменение, которое мы внесли для v0.136, заключается в том, что теперь любой универсальный escape-код обрабатывается как ошибка, поэтому в дополнение к ошибке «T несовместимо с T», Flow также выдает более полезную ошибку на сайте, где общий тип фактически ускользает:

4:     external_var = x;
                      ^ Cannot assign `x` to `external_var` 
                        because type variable `T` [1] cannot 
                        escape from the scope in which it was 
                        defined [2] (try adding a type 
                        annotation to external_var` [3]). 
                        [escaped-generic]
References:
2: function f<T>(x: T, should_escape: bool): T | number {
                    ^ [1]
2: function f<T>(x: T, should_escape: bool): T | number {
              ^ [2]
1: let external_var = 42;
       ^ [3]

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

Как обновить с помощью автоматического добавления аннотаций

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

Чтобы добавить аннотации для исправления escaped-generics, вы можете выполнить следующую команду:

flow codemod annotate-escaped-generics --write /path/to/folder

Эта команда обновляет проект на /path/to/folder на месте. Это, скорее всего, исправит многие ошибки экранированных универсальных шаблонов, но требует дополнительных действий вручную: все еще могут быть некоторые аннотации, которые необходимо добавить вручную, в некоторых случаях новые аннотации сами вызовут ошибки, а некоторые программы (программа, показанная выше , например) могут потребоваться более значительные изменения, потому что никакая аннотация не позволит программе выполнять проверку типов, как написано.

См. документацию для annotate-exports codemod для получения дополнительных указаний по использованию команды codemod.