Spec-Zone.ru › Flow

Flow для пользователей TypeScript

Flow и TypeScript используют большую часть одного и того же синтаксиса, многие одинаковые термины и множество общих концепций (условные типы, отображаемые типы, проверки типов, keyof, as const, unknown, Readonly). За последние несколько лет синтаксис Flow менялся, чтобы соответствовать синтаксису TypeScript. Если вы знаете TypeScript, вашей интуиции будет достаточно, чтобы разобраться в большинстве программ на Flow.

Это Flow или TypeScript? Не скажешь.

type User = {
  readonly name: string,
  readonly age: number,
  readonly metadata: unknown,
};


function get<K extends keyof User>(
  user: User,
  key: K,
): User[K] {
  return user[key];
}


declare const user: User;
const age: number = get(user, 'age');

В тех случаях, когда языки расходятся, это обычно осознанный выбор Flow в пользу более строгих статических гарантий. Flow отвергает ряд шаблонов, которые принимает TypeScript, но которые могут привести к ошибке во время выполнения, незаметно повредить значения или вызвать логические ошибки. В Flow также есть несколько возможностей, не имеющих встроенных аналогов в TypeScript; они описаны ниже в разделе Концепции, характерные только для Flow. Самая заметная из них — React: Flow поддерживает собственный синтаксис первого класса для component, hook и renders.

Эта страница разделена на четыре части:

  • Концепции TypeScript, которые переносятся без изменений — начните отсюда, чтобы ознакомиться с синтаксисом и концепциями, которые можно использовать повторно напрямую.
  • Общие концепции, разные правила — если Flow отклоняет код, написанный в стиле TypeScript, вероятнее всего, причина найдётся здесь.
  • Концепции, характерные только для Flow и не имеющие встроенного аналога в TypeScript.
  • Возможности, характерные только для TypeScript и отсутствующие в Flow.

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

Примечание об области применения: В отличие от tsc, сам исполняемый файл flow — это только проверка типов: он не генерирует JavaScript. Преобразование синтаксиса Flow в JavaScript для выполнения обеспечивается отдельными средствами сборки. TypeScript объединяет обе функции в одном инструменте. Утверждения о TypeScript ниже проверены для версии 6.0.3 с включённым параметром strict.

Концепции TypeScript, которые переносятся без изменений​

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

  • Условные типы с infer, включая форму ограничения infer T extends Bound.
  • Отображаемые типы, включая удаление необязательности с помощью -?.
  • Проверки типов в форме param is T (с дополнительными проверками), включая вывод предикатов типа.
  • Типы доступа по индексу T[K].
  • Оператор keyof T.
  • Утверждения as const.
  • Параметры типа const — function f<const T>(x: T): T.
  • Верхний тип unknown.
  • Ограничения обобщённых типов с помощью <T extends Bound>.
  • Обобщённые типы с параметрами по умолчанию можно указывать без аргументов типа.
  • Вспомогательные типы: Readonly, ReadonlyArray, ReadonlyMap, ReadonlySet, Pick, Omit, Record, Partial, Required, Exclude, Extract, NonNullable, Parameters, ReturnType, NoInfer, Awaited, ThisParameterType, OmitThisParameter, ConstructorParameters, InstanceType, Uppercase, Lowercase, Capitalize, Uncapitalize.
  • Импорт и экспорт только типов — import type и export type.
  • Кодирование перегрузок функций через пересечение — в обоих языках ((x: number) => string) & ((x: string) => number) определяет тип возвращаемого значения для каждого вызова на основе типа аргумента.
  • Формы внешних объявлений, такие как declare const, declare let, declare class и declare function.

Ещё две синтаксические формы напрямую совпадают с TypeScript, хотя лежащая в их основе семантика различается — см. раздел Вариантность.

  • Сокращённая запись массивов T[] (в дополнение к Array<T>).
  • Ключевые слова вариантности: readonly для свойств и in / out для параметров типа.

Обобщённые типы, у всех параметров которых есть значения по умолчанию, можно указывать без аргументов типа​

Как и TypeScript, Flow позволяет опустить список аргументов обобщённого типа, если для каждого параметра типа задано значение по умолчанию. Форма без аргументов Foo применяет значения по умолчанию так же, как и Foo<>. Если для какого-либо параметра типа значение по умолчанию не задано, необходимые аргументы всё равно нужно указать.

type Foo<T = string> = {x: T};
type A = Foo;   // OK - uses the default `T = string`
type B = Foo<>; // OK - equivalent explicit form


const a: A = {x: "hello"};
const b: B = a;

Общие концепции, разные правила​

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

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

  • Объекты, классы и интерфейсы — точные объекты, номинальные классы, асимметричная подтипизация классов, объектов и интерфейсов
  • Вариантность — readonly, writeonly, in и out
  • Запись типов — void и undefined, ?T, empty, unknown
  • Уточнение типов и проверки на уровне модуля — проверка тел функций проверки типов, аннулирование уточнений типов, аннотации на границах модулей
  • Явное управление типами — приведения as и подавление ошибок
Номинальная и структурная типизация.

TypeScript в основном использует структурную типизацию: два типа с одинаковой открытой структурой взаимозаменяемы; есть лишь несколько узких исключений для номинальной типизации (поля #private, модификаторы private/protected и unique symbol). В Flow структурно типизируются обычные объекты и функции, но для классов, непрозрачных типов и перечислений Flow намеренно используется номинальная типизация.

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

Объекты, классы и интерфейсы​

Как Flow типизирует объекты, классы и интерфейсы: правило точности объектов по умолчанию, номинальная идентичность классов, асимметричные правила подтипизации, присваиваемость примитивов интерфейсам, разворачивание на уровне типов, правила привязки this для методов классов и литералов объектов, а также разворачивание кортежей.

Особенность TypeScript Flow
Точность объектов {x: number} разрешает дополнительные свойства, за исключением проверки избыточных свойств для новых литералов объектов. {x: number} по умолчанию точен; используйте {x: number, ...}, если допускаются дополнительные свойства.
Подтипизация классов, интерфейсов и объектов Классы, интерфейсы и типы объектов в основном взаимозаменяемы по структуре. Классы номинальны; типы объектов принимают только объекты своего типа; интерфейсы могут принимать все три вида
Предложения implements / extends Можно указывать вспомогательные типы с формой объекта, например Pick<T, K> или Omit<T, K>. Необходимо указать интерфейс или класс, но не псевдоним типа объекта.
Примитивы и интерфейсы Примитивы соответствуют формам объектов и интерфейсов, существующим в упакованном прототипе. Примитивы не являются подтипами типов объектов или интерфейсов.
Объединение объектов Пересечения — стандартный способ объединения типов объектов. Используйте разворачивание типа объекта ({...A, ...B}); пересечения, хотя и поддерживаются для неточных объектов, не работают для точных объектов.
Привязка this Извлечение метода небезопасно теряет this; this в литералах объектов разрешён. Извлечение метода класса отклоняется; this в литералах объектов запрещён.
Разворачивание кортежа после необязательного элемента Разрешено; если необязательный элемент отсутствует, тип результирующего кортежа не соответствует расположению элементов во время выполнения. Отклоняется, если количество элементов исходного кортежа нельзя определить статически.

Точность объектов​

В Flow типы объектов по умолчанию точны: {x: number} допускает ровно свойство x и никакие другие. Чтобы разрешить дополнительные свойства, используйте неточную форму с завершающим ...: {x: number, ...}. В TypeScript типы объектов открыты на уровне системы типов: {x: number} разрешает дополнительные свойства, а проверка, выявляющая лишние свойства, — это проверка избыточных свойств, которая срабатывает только при прямом присваивании литерала объекта.

Это различие важно, поскольку оно объясняет, почему код TypeScript может выглядеть так, будто он учитывает точность Flow, хотя это не так: const o: {x: number} = {x: 1, y: 2} вызывает ошибку и в TS, но только потому, что литерал встроен напрямую. Косвенные случаи (сначала присваивание литерала переменной, передача его через функцию или другие ситуации, когда литерал теряет статус «нового») проходят проверку типов в TS и не нарушают там правило точности. В Flow точность применяется единообразно независимо от формы привязки, что проявляется в двух шаблонах в стиле TS ниже.

• Шаблон: дополнительные свойства проходят при косвенном присваивании.

// TypeScript:
type Settings = {
  volume: number,
  brightness: number,
};
const raw = {
  volume: 0.5,
  brightness: 0.8,
  theme: "dark",
};
const settings: Settings = raw;
Object.values(settings).map(v =>
  v.toFixed(2), // Runtime crash! `Settings` types every value as `number`, so `.toFixed` runs on the `"dark"` string
);

TypeScript принимает этот код: raw выводится как {volume: number, brightness: number, theme: string}, который структурно является подтипом {volume: number, brightness: number}, поскольку типы объектов TS открыты. При косвенном присваивании проверка избыточных свойств не срабатывает. Flow отклоняет тот же код:

// Flow:
type Settings = {
  volume: number,
  brightness: number,
};
const raw = {
  volume: 0.5,
  brightness: 0.8,
  theme: "dark",
};
const settings: Settings = raw; // ERROR

Если дополнительные свойства добавлены намеренно, решение — использовать неточную форму: написать {volume: number, brightness: number, ...}, чтобы тип явно допускал неизвестные дополнительные свойства:

// Flow:
type Settings = {
  volume: number,
  brightness: number,
  ...
};
const raw = {
  volume: 0.5,
  brightness: 0.8,
  theme: "dark",
};
const settings: Settings = raw; // OK

• Шаблон: свойство забывается, а затем возвращается с другим типом. Это типичный случай при переносе в Flow функции с сигнатурой TypeScript, которая принимает более «свободный» тип и добавляет необязательные поля. TypeScript позволяет забыть свойство посредством неточной подтипизации, а затем вернуть его с другим (необязательным) типом; в результате timestamp типизирован как number | undefined, хотя фактически содержит строку '2026-01-15':

// TypeScript:
type LogEntry = {
  id: string,
  timestamp: string,
};
type WithUnixTime = {
  id: string,
  timestamp?: number,
};

const e: LogEntry = {
  id: 'a',
  timestamp: '2026-01-15',
};
const base: {id: string} = e;       // `timestamp` "forgotten"
const widened: WithUnixTime = base; // `timestamp` re-introduced at a new type
widened.timestamp?.toFixed(); // Runtime crash! ?. only short-circuits on nullish; the value
                              // is the string '2026-01-15', so .toFixed runs and throws.

Flow блокирует этот путь в двух местах: прямой перевод завершается ошибкой на шаге забывания ({id: string} по умолчанию точен), а моделирование забывания с помощью неточного целевого типа переносит ошибку на шаг возвращения свойства. Точность ограничивает оба направления: свойство можно забыть, только если целевой тип неточный, а вернуть — только если исходный тип точный.

// Flow:
type LogEntry = {
  id: string,
  timestamp: string,
};
type WithUnixTime = {
  id: string,
  timestamp?: number,
};


declare const e: LogEntry;


// Literal Flow translation: fails at the forget step because `{id: string}` is exact.
const baseExact: {id: string} = e; // ERROR


// Modeled with inexact target: forget step passes, but re-introduction fails.
const baseInexact: {id: string, ...} = e;     // OK
const widened: WithUnixTime = baseInexact;    // ERROR

Классы номинальны; подтипизация объектов, интерфейсов и классов асимметрична​

TypeScript типизирует классы структурно: interface, type и экземпляры классов в основном взаимозаменяемы, если их структуры совпадают. В Flow тип объекта описывает обычный объект (в частности, структуру, создаваемую литералом объекта {...}), а интерфейс описывает контракт, которому может соответствовать любой обычный объект, экземпляр класса или значение с типом интерфейса. Из этих определений, вместе с номинальной типизацией классов в Flow (два разных класса с одинаковыми членами не взаимозаменяемы), напрямую следуют односторонние правила подтипизации:

  • Литерал объекта преобразуется в тип объекта или интерфейс: его структура и вид полностью известны в момент создания.
  • Экземпляр класса преобразуется в интерфейс (контракт не утверждает, что именно лежит в его основе), но не в тип объекта (типы объектов описывают литералы объектов, а не экземпляры).
  • Значение с типом интерфейса преобразуется в другой интерфейс, но не в тип объекта: в его основе может лежать экземпляр класса, а тип объекта не принимает такие значения.

Неточность целевого типа ({a: number, ...}) расширяет множество допустимых свойств обычных объектов, но не расширяет виды принимаемых значений: типы объектов описывают только обычные объекты. Экземпляры классов и интерфейсы относятся к другим видам и сами по себе всегда неточны (интерфейсы по определению; экземпляры классов — поскольку подкласс может добавлять члены), поэтому они всё равно не соответствуют неточному типу объекта, хотя сообщение об ошибке будет другим:

  • Для точного типа объекта (по умолчанию) ошибка — [incompatible-exact].
  • Для неточного типа объекта ({a: number, ...}) ошибка — [class-object-subtyping] с текстом «Экземпляры классов не являются подтипами типов объектов; попробуйте переписать тип объекта как интерфейс».
// Flow:
class Foo {
  a: number = 1;
}
interface I { a: number }
type Obj = {a: number, ...};


declare function acceptsInterface(
  x: I,
): void;
declare function acceptsObj(
  x: Obj,
): void;
declare const someI: I;


acceptsInterface(new Foo()); // OK
acceptsInterface({a: 1});    // OK
acceptsObj({a: 1});          // OK
acceptsObj(new Foo());       // ERROR
acceptsObj(someI);           // ERROR: same [class-object-subtyping] code

Стандартное решение в Flow — заменить тип параметра с типа объекта на интерфейс.

Из различия видов следует ещё одно ограничение: типы объектов не допускают this (Flow отклоняет this в литералах объектов с ошибкой [object-this-reference]), поэтому они описывают обычные данные, а не поведение с учётом this. Именно поэтому для implements/extends необходим класс или интерфейс, а извлечение методов безопасно для типов объектов, но не для экземпляров классов.

Ещё одно замечание: структурная типизация классов в TypeScript почти всеобъемлюща: const c: C = {x: 1} проходит проверку типов в TS, хотя c аннотирован как экземпляр класса. Помимо структурной типизации по умолчанию, TS содержит несколько исключений для номинальной типизации: поля ECMAScript #private, модификаторы доступа private / protected (оба препятствуют присваиванию между разными объявлениями) и unique symbol — но это исключения из в остальном структурной модели. Номинальность классов в Flow абсолютна: явное включение номинальной типизации не требуется, поскольку идентичность самого класса служит номинальным признаком. Именно поэтому ошибка Flow о классе и объекте часто удивляет пользователей TS.

В предложениях implements и extends нужно указывать интерфейс или класс​

В Flow правая часть предложения implements или extends подчиняется более строгим правилам формы, чем в TypeScript. TypeScript позволяет написать class C implements Omit<HTMLAttrs, "k"> или interface I extends Pick<Y, K> — подойдёт любой тип объекта. Flow отклоняет типы объектов в таких предложениях и выдаёт соответствующие сообщения об ошибках:

  • class C implements ObjType вызывает ошибку [cannot-implement]: «Невозможно реализовать ObjType, поскольку это не интерфейс».
  • interface I extends ObjType вызывает ошибку [incompatible-use]: «Невозможно расширить ObjType …, поскольку ObjType нельзя наследовать».

Стандартное исправление в Flow — объявить интерфейс (сначала interface I { a: number; b: string }, затем class C implements I) или встроить члены напрямую. В Flow можно применять отображаемые и вспомогательные типы к интерфейсам, но результатом будет тип объекта, который именно эти предложения не принимают. Поэтому результатом переписывания должен быть интерфейс, а не псевдоним с типом объекта.

// Flow:
type ObjType = {a: number, b: string};
class C implements ObjType { // ERROR: [cannot-implement]
  a: number = 1;
  b: string = "hi";
}
interface I extends ObjType { // ERROR: [incompatible-use]
  c: boolean;
}

Примитивы не являются подтипами интерфейсов или типов объектов​

TypeScript считает string / number / boolean структурно присваиваемыми любому интерфейсу или типу объекта, которому они соответствуют: примитив проверяется по членам соответствующего упакованного прототипа (String.prototype / Number.prototype / Boolean.prototype), поэтому string соответствует любому интерфейсу, члены которого присутствуют в String.prototype (например, {length: number}, Iterable<string>). Это не подразумевает упаковку во время выполнения: совместимость существует только на уровне типов. В файлах .js Flow не выполняет такую проверку: преобразование примитива в интерфейс вызывает ошибку [incompatible-type] («Невозможно использовать string как подтип интерфейса»), а преобразование примитива в тип объекта — общую ошибку с кодом [incompatible-type].

Обычно следствием во время выполнения становится логическая ошибка. Типу Iterable<string> соответствуют и Array<string>, и string: TS принимает последний вариант, поэтому вызывающая сторона может незаметно передать обычную строку вместо списка строк. Тогда цикл for...of выдаёт символы, а не элементы.

// TypeScript:
function logAll(
  items: Iterable<string>,
) {
  for (const x of items) console.log(x);
}
logAll(["foo", "bar"]); // logs "foo" then "bar"
logAll("fb");           // String.prototype[Symbol.iterator] yields characters,
                        // so the loop walks code points: logs "f" then "b" instead of items

TypeScript принимает этот код благодаря продвижению к типу оболочки: String.prototype[Symbol.iterator] соответствует Iterable<string>, поэтому string передаётся так же, как массив. Flow отклоняет код:

// Flow:
function logAll(
  items: Iterable<string>,
) {
  for (const x of items) console.log(x);
}
const msgs = "foo";
logAll(msgs); // ERROR: `string` is not a subtype of `Iterable<string>`

В Flow исправление нужно внести в месте вызова: если подразумевался список из одного элемента, оберните строку в [s]; иначе вызов имеет неверную форму.

Типизация разворачивания объектов​

Flow поддерживает {...A, b: T} на уровне типов: type C = {...A, b: T} — это настоящая аннотация типа, объединяющая свойства A с b: T. В TypeScript разворачивание на уровне типов отсутствует; вместо него используется пересечение (type C = A & {b: T}).

Это не просто стилистический выбор: он обусловлен точными типами объектов. Поскольку точные типы объектов Flow запрещают неуказанные свойства, пересечение двух точных типов объектов создаёт невозможный тип: значение должно было бы быть в точности A и в точности B одновременно, что невозможно, если A и B хоть чем-то различаются (см. раздел невозможные типы пересечений). Поэтому Flow нужна другая операция для объединения точных типов объектов. Такой операцией служит разворачивание на уровне типов ({...A, ...B}), которое непосредственно повторяет семантику разворачивания значений во время выполнения: учитываются только собственные свойства (поэтому интерфейсы нельзя разворачивать, так как они не отслеживают различие между собственными свойствами и свойствами прототипа), последующие ключи перезаписывают предыдущие, а если разворачивается неточный тип, результирующий тип также необходимо пометить как неточный (разворачивание должно стоять перед любыми именованными свойствами). Иначе Flow отклонит разворачивание, поскольку неизвестные свойства источника нельзя безопасно добавить в точный целевой тип.

// Flow:
type A = {x: number, y: string};
type C = {...A, z: boolean};
const c: C = {x: 1, y: "hi", z: true};

Типизация разворачивания значений в Flow также безопаснее. TypeScript типизирует {...c} так, будто копируются все свойства интерфейса, но если в основе значения лежит класс, методы прототипа фактически не попадут в результат разворачивания во время выполнения, поэтому такая типизация небезопасна:

// TypeScript:
interface Counter {
  count: number;
  incr(): number;
}
class Impl {
  count = 0;
  incr(): number { return ++this.count; }
}
const c: Counter = new Impl();
const o = {...c};
o.incr(); // Runtime crash! `incr` is on `Impl.prototype`, not on the spread result

Flow отклоняет код с ошибкой [cannot-spread-interface], поскольку интерфейсы не отслеживают различие между собственными свойствами и свойствами прототипа, поэтому тип результата не может быть безопасным:

// Flow:
interface Counter {
  count: number;
  incr(): number;
}
declare const c: Counter;
const o = {...c}; // ERROR: [cannot-spread-interface]

В методах классов this привязан, а в литералах объектов запрещён​

Оба правила Flow, связанные с this, предотвращают одну и ту же ошибку во время выполнения: выполнение тела метода с неопределённым значением this. Для классов запрещается извлечение, чтобы вызов оставался привязан к своему получателю; для литералов объектов запрещается само создание, чтобы в обычном объекте изначально не было метода, учитывающего this, который можно было бы извлечь.

Использование this Извлечение метода
Классы Разрешено Запрещено
Литералы объектов Запрещено Разрешено

• Извлечение метода из экземпляра класса. TypeScript рассматривает методы как обычные значения функций и позволяет извлекать их без предупреждения: при вызове извлечённой версии тело метода выполняется с неопределённым значением this, и любой доступ через this.field приводит к ошибке.

// TypeScript:
class Counter {
  count = 0;
  incr(): number {
    return ++this.count;
  }
}
const counter = new Counter();
const tick = counter.incr;
tick(); // Runtime crash! `this` is undefined, so `++this.count` throws

Flow отклоняет извлечение с ошибкой [method-unbinding] («Невозможно получить counter.incr, поскольку свойство incr нельзя отвязать от контекста, в котором оно было определено»), так как отслеживает привязку this для свойств, записанных в сокращённой форме метода, в классе:

// Flow:
class Counter {
  count: number = 0;
  incr(): number {
    return ++this.count;
  }
}
const counter = new Counter();
const tick = counter.incr; // ERROR: [method-unbinding]
const tickFixed = () =>
  counter.incr(); // OK - arrow captures `this`
tickFixed(); // OK

В Flow нужно обернуть вызов в стрелочную функцию, захватывающую this, как показано выше на примере tickFixed.

• this внутри литерала объекта. TypeScript разрешает ссылки на this внутри методов литералов объектов (TS выводит this как тип охватывающего литерала); та же опасность извлечения возникает затем в месте вызова.

// TypeScript:
const counter = {
  count: 0,
  incr(): number {
    return ++this.count;
  }
};
const tick = counter.incr;
tick(); // Runtime crash! `this` is undefined, so `++this.count` throws

Flow запрещает саму конструкцию с ошибкой [object-this-reference], поэтому выполнить небезопасное извлечение не удастся:

// Flow:
const counter = {
  count: 0,
  incr(): number { return ++this.count; } // ERROR: [object-this-reference]
};

В Flow вместо this нужно напрямую использовать имя привязки литерала объекта.

// Flow:
const counter = {
  count: 0,
  incr(): number {
    return ++counter.count;
  } // Use object name directly instead of `this`
};
const tick = counter.incr; // Extraction allowed: no `this` in the function
tick(); // OK

Сокращённая запись метода в обычных типах объектов ({m(x: number): number}) не несёт контекста this, который можно было бы потерять, поэтому извлечение здесь безопасно. Этим также объясняется, почему экземпляр класса нельзя преобразовать в тип объекта, а литерал объекта можно.

Разворачивание кортежа после необязательного элемента запрещено​

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

// TypeScript:
const middle: [middleName?: string] = [];
const fullName: [
  string,
  string | undefined,
  string,
] = ["Ada", ...middle, "Lovelace"];
fullName[2].toUpperCase(); // Runtime crash! `fullName[2]` is undefined, not "Lovelace"

Тип TS известен статически (fullName аннотирован как [string, string | undefined, string], и эта аннотация принимается), но он небезопасен: если во время выполнения middle пуст, значение в позиции 1 — это "Lovelace" (со смещением), а позиция 2 отсутствует. Поэтому кортеж, вычисленный TS, не соответствует расположению элементов во время выполнения. Flow отклоняет разворачивание с ошибкой [invalid-tuple-arity] («литерал массива содержит неизвестное количество элементов»), поскольку количество элементов исходного кортежа нельзя определить статически, а значит, для результата нельзя создать безопасную статическую форму кортежа:

// Flow:
const middle: [middleName?: string] = [];
const fullName: [
  string,
  string | void,
  string,
] = ["Ada", ...middle, "Lovelace"]; // ERROR

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

Вариантность​

В Flow правила вариантности по умолчанию строже, чем в TypeScript. В подразделах ниже рассматриваются ключевые слова, позволяющие явно указать вариантность или инвариантность, а также позиции, в которых правила по умолчанию различаются.

Особенность TypeScript Flow
Ключевые слова вариантности Использует свойства readonly и параметры типа in / out; явную инвариантность также можно указать как <in out T>. Использует свойства readonly / writeonly и параметры типа in / out; по умолчанию параметры типа инвариантны.
Изменяемые свойства объектов Ковариантны. Инвариантны.
Присваиваемость свойств readonly readonly и изменяемые свойства присваиваются таким образом, что ограничение только для чтения может быть утрачено. readonly нельзя отменить, присвоив свойству изменяемый тип.
Изменяемые массивы Ковариантны. Инвариантны.
Аргументы обобщённых типов Вариантность выводится из использования, с исключениями ради совместимости. По умолчанию инвариантны, если не объявлены как out или in.
Параметры методов Бивариантны для синтаксиса методов; поля с типом функции контравариантны. Контравариантны.
Позиции типа this Допустимы в позициях ввода и инвариантных позициях (изменяемое поле). Ограничены ковариантными позициями (типы возвращаемых значений и поля readonly).
Краткий обзор вариантности.

Вариантность описывает, как подтипирование распространяется через позицию, в которой появляется T, например тип свойства в {x: T}, параметр или возвращаемый тип функции либо аргумент обобщённого типа, например Container<T>. Если Sub является подтипом Super, эта позиция бывает:

  • Ковариантной: сохраняет направление. {readonly x: Sub} является подтипом {readonly x: Super}. Подходит для позиций только для чтения и возвращаемых типов функций.
  • Контравариантной: меняет направление на противоположное. Функция (x: Super) => void является подтипом (x: Sub) => void: вызываемая функция, принимающая более широкий набор входных значений, удовлетворяет требованиям вызывающего кода, передающего более узкий набор.
  • Инвариантной: ни одно направление не подходит; позицию нельзя безопасно расширить или сузить. Это необходимое значение по умолчанию для слота, который и читается, и записывается (например, изменяемого {x: T}), поскольку ковариантность нарушает корректность записи, а контравариантность — чтения.
  • Бивариантной: допустимы оба направления. Обычно это небезопасно; TypeScript разрешает это в нескольких местах (в частности, для параметров методов). Flow никогда не использует бивариантность.

Flow по умолчанию выбирает для каждой позиции наиболее строгий безопасный вариант; TypeScript в нескольких позициях по умолчанию выбирает менее строгие варианты.

Ключевые слова вариантности (readonly / writeonly, in / out)​

В стандартном синтаксисе Flow для обозначения вариантности используются ключевые слова, согласованные с TS: readonly / writeonly для свойств и in / out для параметров типов. Обратите внимание: writeonly существует только в Flow — в TypeScript нет эквивалента для записи только.

В обратную сторону: у комбинированного <in out T> в TypeScript (явная инвариантность) нет аналога в Flow. TypeScript выводит вариантность из использования и сохраняет ряд исключений ради совместимости, поэтому иногда пользователям приходится явно возвращаться к инвариантности, чтобы получить нужную более строгую гарантию; по умолчанию Flow использует инвариантность, поэтому при отсутствии указания вы получите именно более строгий вариант. Подробнее о различии значений по умолчанию см. ниже в разделе Аргументы обобщённых типов.

Помимо различий в написании, Flow проверяет, что параметр типа, объявленный как out T (или in T), используется в теле только в позициях, соответствующих объявленной вариантности: использование out T во входной позиции вызывает ошибку [incompatible-variance] «Нельзя использовать T во входной позиции, поскольку ожидается, что T будет встречаться только в выходных позициях». TypeScript также во многих позициях проверяет соответствие in / out телу (например, interface Box<out T> { set: (t: T) => void } также вызывает ошибку в TS, поскольку типизированное функцией поле помещает T в контравариантную позицию: входные параметры функции меняют вариантность на противоположную). Более узкое различие состоит в том, что TS сохраняет бивариантность сокращённой записи метода, даже если указана аннотация out/in, поэтому приведённая ниже форма Flow (записанная с помощью сокращённой записи метода) вызывает ошибку в Flow, но компилируется в TS.

// Flow:
type Box<out T> = {
  set(t: T): void; // ERROR: [incompatible-variance]
};

В этом подразделе речь идёт о синтаксисе; о гораздо более важном семантическом различии в том, как вариантность обеспечивается в каждой позиции, см. следующий подраздел. Полное описание механики см. в документации по вариантности.

Каждый из приведённых ниже подразделов описывает ситуацию, в которой Flow выбирает более строгий безопасный вариант по умолчанию, а TypeScript — менее строгий. Вместе они составляют большую группу кода TypeScript, проходящего проверку типов, но опирающегося на более слабые статические гарантии: каждый пример допускает программу, которая может вызвать исключение во время выполнения или незаметно повредить значения.

Изменяемые свойства объектов инвариантны в Flow и ковариантны в TS​

Присваивание {price: number} в {price: number | string} расширяет тип слота при чтении, но также расширяет набор значений, которые можно в него записать. Поэтому последующий p.price = "Free!" помещает строку в свойство вызывающего кода с типом number.

// TypeScript:
function markFree(p: {
  price: number | string,
}) {
  p.price = "Free!";
}
const item: {price: number} = {
  price: 9.99,
};
markFree(item);
item.price.toFixed(2); // Runtime crash! `.toFixed` is not a function on `"Free!"`

TypeScript разрешает вызов: свойство ковариантно, поэтому {price: number} считается подтипом {price: number | string}. Затем изменение внутри markFree записывает строку в слот вызывающего кода с типом number. Flow отклоняет такой код:

// Flow:
function markFree(p: {
  price: number | string,
}) {
  p.price = "Free!";
}
const item: {price: number} = {
  price: 9.99,
};
markFree(item); // ERROR: property `price` is invariantly typed

Способ исправления зависит от того, действительно ли функции нужно вносить изменения. Если нет, сделайте параметр доступным только для чтения (с помощью утилиты Readonly<T> или модификатора свойства readonly); запрет на изменение через p делает расширение безопасным. Если изменения нужны (как здесь, в markFree), вызывающий код должен предоставить источник, аннотация которого уже включает более широкий тип. TypeScript поддерживает тот же модификатор свойства readonly, но о различии между языками в том, как обеспечивается его соблюдение, см. следующий подпункт.

// Flow:
function logPrice(
  p: Readonly<{price: number | string}>,
) {}
const item: {price: number} = {
  price: 9.99,
};
logPrice(item); // OK

Свойства readonly взаимозаменяемы с изменяемыми в TS, но не в Flow​

Присваивание {readonly value: T} в {value: T} позволило бы вызывающему коду снять ограничение «только для чтения» и изменить базовый объект.

// TypeScript:
function f(obj: {value: number}) {
  obj.value = 99; // Silent mutation of a slot the caller annotated `readonly`
}
const o: {readonly value: number} = {
  value: 1,
};
f(o);

TypeScript разрешает вызов; изменение через f успешно выполняется во время выполнения. TypeScript обеспечивает соблюдение readonly непосредственно в местах записи, но при проверке совместимости типов ограничение readonly может быть снято. Flow считает readonly / writeonly важными для статической безопасности и отклоняет такой код:

// Flow:
function f(obj: {value: number}) {
  obj.value = 99;
}
const o: {readonly value: number} = {
  value: 1,
};
f(o); // ERROR: [incompatible-variance]

Исправление — также пометить целевой объект как доступный только для чтения ({readonly value: number}): если f заявляет, что не будет изменять объект, снятие ограничения больше не имеет значения, и вызов проходит. Если f действительно нужно вносить изменения, вызывающий код должен передать изменяемый источник.

// Flow:
function f(
  obj: {readonly value: number},
) {}
const o: {readonly value: number} = {
  value: 1,
};
f(o); // OK

Изменяемые массивы инвариантны в Flow и ковариантны в TS​

Если считать Array<string> типом Array<string | Error>, то .push(new Error(...)) внутри вызываемой функции сможет поместить Error в массив вызывающего кода, типизированный строками.

// TypeScript:
function appendError(
  es: Array<string | Error>,
) {
  es.push(new Error("oops"));
}
const errors: Array<string> = [];
appendError(errors);
errors[0].toUpperCase(); // Runtime crash! `errors[0].toUpperCase` is not a function

TypeScript разрешает вызов. Операция push выполняется, и элемент с типом string по индексу 0 на самом деле оказывается экземпляром Error во время выполнения. Flow отклоняет такой код:

// Flow:
function appendError(
  es: Array<string | Error>,
) {
  es.push(new Error("oops"));
}
const errors: Array<string> = [];
appendError(errors); // ERROR: `Array<string>` is invariantly typed

Способ исправления зависит от того, нужно ли вызываемой функции изменять массив. Если нет, замените тип параметра на ReadonlyArray<T>: запрет на изменение через es делает расширение безопасным. ReadonlyArray существует в Flow именно потому, что изменяемая форма инвариантна — об этом часто забывают, выбирая ковариантный шаблон TS. Если вызываемой функции действительно нужно вносить изменения (как в случае с appendError), вызывающий код должен передать массив, тип элементов которого уже включает более широкий вариант.

// Flow:
function logErrors(
  es: ReadonlyArray<string | Error>,
) {}
const errors: Array<string> = [];
logErrors(errors); // OK

Аргументы обобщённых типов по умолчанию инвариантны​

Flow по умолчанию считает параметры обобщённых типов инвариантными и требует явно указывать ковариантность или контравариантность с помощью out T / in T. TypeScript выводит вариантность из использования и сохраняет исключения ради совместимости, из-за чего для полей, доступных и для чтения, и для записи, статические гарантии могут оказаться слабее, чем при значении по умолчанию в Flow.

// TypeScript:
class Box<T> {
  value: T;
  constructor(value: T) {
    this.value = value;
  }
}
function widen(
  b: Box<number | string>,
) {
  b.value = "oh no";
}
const box: Box<number> = new Box(1);
widen(box);
box.value.toFixed(); // Runtime crash! `box.value` is now "oh no", not a number

TypeScript разрешает вызов; затем widen записывает строку в слот вызывающего кода, типизированный числом. Flow отклоняет такой код:

// Flow:
class Box<T> {
  value: T;
  constructor(value: T) {
    this.value = value;
  }
}
function widen(
  b: Box<number | string>,
) {}
const box: Box<number> = new Box(1);
widen(box); // ERROR

При написании обобщённого типа Flow в такой форме возможны два варианта: поле действительно доступно только для чтения (пометьте его как readonly, а параметр — как out) или это не так, и тогда инвариантность Flow корректна.

Параметры методов контравариантны в Flow, но бивариантны в TS​

Параметры методов в TypeScript бивариантны: {compare(a: string, b: string): number} считается подтипом {compare(a: string | number, b: string | number): number}, поэтому более широкий тип может передать числа в тело, которое умеет обрабатывать только строки, что приведёт к сбою во время выполнения. (Поля, типизированные функцией, в TypeScript контравариантны: эта асимметрия между методами и полями — особенность только TS.)

// TypeScript:
type StringComparator = {
  compare(a: string, b: string): number,
};
type Comparator = {
  compare(
    a: string | number,
    b: string | number,
  ): number,
};

function callCompare(c: Comparator) {
  c.compare(1, 2); // Runtime crash! body calls .localeCompare on a number
}
const stringComparator: StringComparator = {
  compare(a, b) {
    return a.localeCompare(b);
  }
};
callCompare(stringComparator);

Проблема бивариантности незаметна в месте вызова: пользователи TS не видят ошибку до момента выполнения. Flow статически отклоняет вызов и обнаруживает ошибку до сбоя:

// Flow:
type StringComparator = {
  compare(a: string, b: string): number,
};
type Comparator = {
  compare(
    a: string | number,
    b: string | number,
  ): number,
};


function callCompare(c: Comparator) {}
const stringComparator: StringComparator = {
  compare(a, b) {
    return a.localeCompare(b);
  }
};
callCompare(stringComparator); // ERROR

Конкретная ошибка Flow зависит от того, записан ли compare сокращённо как метод или как поле с типом функции. Сокращённая запись метода (как выше) нарушает контравариантность ([incompatible-type] «первый параметр: string несовместим с number»): входные параметры функций меняют вариантность на противоположную, поэтому их расширение небезопасно.

Замена сокращённой записи метода на изменяемое поле с типом функции делает проверку более строгой, а не менее строгой. Теперь само свойство изменяемо, поэтому возникает ошибка инвариантности (тип свойства инвариантен), которая блокирует и противоположное (безопасное) направление.

Добавление readonly compare восстанавливает это безопасное направление (значение с типом Comparator передаётся в слот StringComparator), делая свойство ковариантным, но не исправляет приведённый выше пример: расширение входных параметров по-прежнему блокируется контравариантностью входов функции, и единственный способ принимать более широкие входные значения — изначально объявить compare с такими входными значениями.

Полное описание механики см. в документации по вариантности и документации по подтипированию.

Тип this допускается только в ковариантных позициях​

Тип this используется во fluent API и для полиморфных получателей методов. В ковариантных позициях (возвращаемых типах и полях readonly в Flow) оба языка его принимают: метод, объявленный как m(): this, сохраняет тип подкласса в цепочках fluent-вызовов, поэтому new SubBuilder().add(1).extra() сохраняет свой тип SubBuilder. Различие проявляется во входных и инвариантных (изменяемых полях) позициях. TypeScript также принимает там this, но записываемый слот типа this позволяет вызывающему коду записать экземпляр родительского класса в поле, типизированное подклассом; последующий вызов метода, существующего только в подклассе, приводит к сбою:

// TypeScript:
class Builder {
  parent: this | null = null;
}
class SubBuilder extends Builder {
  extra(): this { return this; }
}
function stash(b: Builder) {
  b.parent = new Builder(); // `b.parent` is `Builder | null` here, so the write is allowed
}
const sb = new SubBuilder();
stash(sb); // `SubBuilder` flows into `Builder`; the write inside `stash` lands on `sb.parent`
if (sb.parent !== null) {
  // The null check only excludes null; it doesn't verify the class identity.
  // The unsound write in `stash` put a `Builder` in a slot typed `this` (= `SubBuilder`).
  sb.parent.extra(); // Runtime crash! `sb.parent` is a `Builder`, not a `SubBuilder`
}

Flow отклоняет this как во входных, так и в инвариантных (изменяемых полях) позициях с ошибкой [incompatible-variance]. Это следует из той же модели вариантности, согласно которой изменяемые свойства объектов и изменяемые массивы инвариантны:

// Flow:
class Builder {
  add(x: number): this { return this; } // OK: return type is covariant
  readonly origin: this | null = null; // OK: readonly field is covariant
  takesSelf(other: this): void {} // ERROR: input position
  parent: this | null = null; // ERROR: invariant field
}

Исправление в этом случае — явно указать класс во входной позиции или позиции поля (other: Builder, parent: Builder | null) и принять потерю типа подкласса в этом слоте либо сделать поле readonly, чтобы позиция стала ковариантной.

Обозначения типов​

Как Flow обозначает отсутствующие значения, nullable-типы, а также верхний и нижний типы иерархии типов. Здесь различаются только названия: понятия те же, обозначения разные.

Понятие TypeScript Flow Примечание
Тип, состоящий только из undefined undefined void В Flow нет отдельного типа undefined. (подробнее)
Маркер возвращаемого значения «нет полезного значения» void void Название то же; в Flow есть только void (см. выше).
Nullable-значение (T | null | undefined) T | null | undefined ?T (сокращение для T | null | void) Одному T | void не хватает null.
Нижний тип never empty never — естественный аналог в TS, когда Flow ожидает empty.
Верхний тип unknown unknown Название то же.

Чтобы понять, какое место эти типы занимают относительно остальных типов Flow, см. иерархию типов.

void и undefined​

Использование undefined в качестве аннотации в Flow приводит к безусловной ошибке:

// Flow:
function f(): undefined { // ERROR: [unsupported-syntax]
  return undefined;
}
// Flow:
function f(): void {
  return undefined; // OK - `undefined` is the value inhabiting `void`
}

Чаще всего это происходит, когда сигнатуру функции в стиле TS дословно переносят в Flow, а также в коде с утилитными типами TS (Exclude<T, undefined>, T extends undefined ? ...), где литеральный тип undefined появляется внутри обобщённого типа. Стандартные варианты Flow: undefined → void для аннотаций; T | undefined → ?T, если предполагается также null (как в большинстве API JavaScript), или T | void, если подразумевается только отсутствие значения.

Стоит также отметить связанный нюанс TS: типы TypeScript () => undefined и () => void асимметричны с точки зрения совместимости типов (возвращаемые значения undefined удовлетворяют требованиям слотов void, но не наоборот). В Flow такого соответствия нет, поскольку существует только void.

Тип параметра, включающий void (независимо от того, записан ли он как T | void, ?T или T | null | void), делает аргумент неявно необязательным, поэтому вызывающий код может полностью его опустить. Это отличается от TypeScript, где (x: T | undefined) по-прежнему требует передать undefined в месте вызова.

// Flow:
function f(x: ?number) {}
f(null);      // OK
f(undefined); // OK
f();          // OK - `?T` includes `void`, which makes the arg optional

Уточнение типов и проверки на уровне модуля​

Как Flow проверяет тела проверок типов, в каких случаях промежуточный код сбрасывает уточнения и какие проверки Flow выполняет на границах модулей (требования к аннотациям и граница между значениями и типами).

Тела пользовательских проверок типов проходят проверку​

TypeScript проверяет сигнатуру предиката x is T: он требует, чтобы тип предиката был совместим с типом параметра, поэтому function f(x: string): x is number отклоняется уже при объявлении. Однако TypeScript не проверяет, действительно ли тело функции реализует заявленное уточнение. Телу доверяют, поэтому следующий код проходит проверку типов в TypeScript, хотя typeof x === "object" && x !== null истинно для любого ненулевого объекта (в том числе для {}), и любой вызывающий код, полагающийся на эту проверку, получит неверную информацию:

// TypeScript:
type User = {id: string, name: string};
function isUser(x: unknown): x is User {
  return typeof x === "object" && x !== null;
}
const data: unknown = {};
if (isUser(data)) {
  data.name.toUpperCase(); // Runtime crash! `data` has no `name` property
}

Flow проверяет тело функции-проверки типов в обоих направлениях. Для каждого направления выдаётся отдельная диагностика.

• Положительное направление ([incompatible-type-guard]). Для каждого выражения return тип уточнённого параметра должен быть подтипом типа проверки. Поэтому аналог приведённого выше примера TypeScript отклоняется:

// Flow:
type User = {id: string, name: string};
function isUser(x: unknown): x is User {
  return typeof x === "object" && x !== null; // ERROR: refined type isn't a subtype of `User`
}

• Отрицательное направление ([incompatible-type-guard]). Когда предикат возвращает false, отрицание должно полностью исключать тип проверки из типа параметра — иначе вызывающий код может получить значение, которое следовало исключить. По этой причине отклоняется предикат, типизированный как x is A, но фактически проверяющий x instanceof B (строгий подтип):

// Flow:
class A {}
class B extends A {}
function isA(x: unknown): x is A {
  return x instanceof B; // ERROR: negation does not refine `A` away
}

В диагностике явно предлагается обходной путь: «Рассмотрите возможность использовать одностороннюю проверку типов (implies x is T)». Односторонние проверки типов (implies x is T) пропускают именно эту проверку отрицания: они уточняют параметр до T, когда функция возвращает true, и оставляют его без изменений, когда функция возвращает false. Это подходящая форма, если верно только положительное направление.

Полные правила согласованности см. в документации по проверкам типов, а форму implies — в следующем разделе Односторонняя проверка типов.

Правила сброса уточнений различаются​

И Flow, и TypeScript сужают типы с помощью typeof, instanceof, сравнений на равенство, проверок типов и т. д., но правила сброса уточнений различаются, и эти различия не всегда очевидны при чтении кода. Flow сбрасывает уточнение, если промежуточный код мог изменить значение в соответствующем месте хранения:

  • Запись в уточнённую переменную или свойство (x = ..., obj.k = ...).
  • Уточнение свойства объекта, доступного через псевдоним или потенциально изменяемого вызываемой функцией.
  • Уточнение переменной, захваченной замыканием, которое может быть вызвано промежуточным вызовом.

Простой вызов функции, которая явно не обращается к уточнённому месту, сам по себе не сбрасывает уточнение локальной переменной: именно здесь чаще всего исправляют код чрезмерно. У сужения типов в TypeScript своя, тоже нетривиальная модель сброса, которая в подробностях не совпадает с моделью Flow; один и тот же код может проходить проверку в TS, но не в Flow, или наоборот. Полный набор правил см. в разделе сброс уточнений.

// Flow:
declare function sideEffect(): void;


function localCase(x: ?number) {
  if (x != null) {
    sideEffect();        // bare call does NOT drop the refinement on a local
    const a: number = x; // OK
  }
}


function propertyCase(obj: {
  x: ?number,
}) {
  if (obj.x != null) {
    sideEffect();            // bare call DROPS the refinement on a property
    const a: number = obj.x; // ERROR: callee could have mutated `obj.x`
  }
}


function writeCase(x: ?number) {
  if (x != null) {
    x = null;
    const a: number = x; // ERROR: `x` is now typed `null` after the write
  }
}

Стандартное исправление для свойств — извлечь уточнённое значение в локальную переменную до выполнения промежуточного кода: для локальной переменной действует описанное выше исключение для простого вызова, поэтому уточнение сохраняется. Проблему с записью можно устранить, не переназначая уточнённую переменную; вместо этого используйте отдельную локальную переменную для нового значения.

// Flow:
declare function sideEffect(): void;


function propertyCaseFixed(obj: {
  x: ?number,
}) {
  const {x} = obj;
  if (x != null) {
    sideEffect();
    const a: number = x; // OK - local refinement survives the call
  }
}

На границах модулей обязательны аннотации​

Flow требует аннотации для параметров функций, экспортов и других важных границ, а также сообщает [signature-verification-failure], если типы экспортов модуля нельзя определить только по аннотациям. Если вы привыкли оставлять экспорты без аннотаций и позволять системе проверки типов выводить их типы между модулями, в Flow это не сработает: аннотации должны присутствовать. В TypeScript есть аналогичная возможность, включаемая по желанию (--isolatedDeclarations, TS 5.5+), но она не входит в strict и по умолчанию выключена.

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

Полное описание механики см. в документации о требованиях к аннотациям и подразделе Экспорты модуля.

// Flow:
// ERROR: return type inferred, not annotated.
export function getUser(id: string) { // [signature-verification-failure]
  return {id, name: 'Alice', age: 30};
}

// OK - annotate the return so the module's typed interface is self-contained.
export function getUser(id: string): {
  id: string,
  name: string,
  age: number,
} {
  return {id, name: 'Alice', age: 30};
}

Связывания, содержащие только типы, не могут пересекать границу между значениями и типами​

В TypeScript import type и export type по умолчанию представляют собой стираемые аннотации: классификация выполняется в месте использования, поэтому import {Foo} в позиции значения для экспорта, содержащего только тип, незаметно интерпретируется как тип. (В режиме --verbatimModuleSyntax TS требует указывать import type в месте импорта и выдаёт ошибку в противном случае, но этот режим включается отдельно и не является режимом по умолчанию.) Flow безусловно проверяет вид значения или типа в местах импорта и экспорта и выдаёт две отдельные диагностики:

  • Импорт import {Foo} в позиции значения из модуля, который только export type-нул Foo, вызывает ошибку [import-type-as-value]: «Невозможно импортировать тип Foo как значение. Вместо этого используйте import type».
  • Использование export {Foo} в позиции значения, когда Foo является связыванием только типа в текущем модуле, вызывает ошибку [type-as-value]: «Невозможно использовать тип Foo как значение. Типы стираются и не существуют во время выполнения».

В обоих случаях исправление одно: использовать явную форму для типа — import type {Foo} или export type {Foo}. Это важно для модели извлечения сигнатур Flow: типизированный интерфейс модуля должен однозначно указывать, какие экспорты являются типами, а какие — значениями, поскольку зависимые модули проверяются параллельно относительно этого интерфейса, без анализа тела модуля.

// Flow:
// mod.js
export type Foo = {x: number};
// Flow:
// consumer.js
import {Foo} from './mod'; // ERROR: [import-type-as-value]
import type {
  Foo as FooType,
} from './mod'; // OK

Явное управление типами​

Два случая, в которых TypeScript допускает менее строгую запись, а Flow требует явную форму: приведения типов as и подавление ошибок.

Приведения as безопаснее в Flow​

Конструкция as в Flow только расширяет тип или утверждает его (например, 42 as number, 42 as 42) и отклоняет небезопасные приведения к более узкому типу на уровне типов: {id: 1} as {id: number, name: string} — это ошибка Flow, а не приведение. Конструкция as в TypeScript принимает любое приведение, если типы совместимы в любом направлении, что позволяет ей молча одобрять небезопасные приведения к более узкому типу. Та же строка TypeScript проходит проверку типов, хотя приведение добавляет несуществующее во время выполнения свойство name: string, и обращение к нему приводит к сбою.

// TypeScript:
const u = {id: 1} as {
  id: number,
  name: string,
};
u.name.toUpperCase(); // Runtime crash! `u.name` is undefined

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

// Flow:
const n = 42 as number; // OK - widens the literal `42` to `number`
const exact = 42 as 42; // OK - asserts at the literal type
const u = {id: 1} as {id: number, name: string}; // ERROR - unsafe downcast

В Flow для принудительного приведения используется явная двухэтапная конструкция value as any as T; идиоматичная форма в TypeScript — value as unknown as T.

Подавление ошибок имеет код и ограниченную область действия​

В Flow конструкция $FlowFixMe[code] (и $FlowExpectedError[code]) подавляет только ошибку с указанным кодом в этом месте: любые другие ошибки в той же строке по-прежнему отображаются, а само подавление отмечается предупреждением о неиспользуемом подавлении, если целевая ошибка не возникла. // @ts-ignore в TypeScript без разбора отключает все ошибки в следующей строке, а // @ts-expect-error аналогичным образом отключает все ошибки, но выдаёт ошибку, если нечего было подавлять. Форма Flow значительно точнее и упрощает отслеживание долга, связанного с подавлениями. См. документацию об ошибках.

// Flow:
const o: {x: number} = {x: 1};
// $FlowFixMe[prop-missing] - intentional for demo
const y = o.nonexistent;

Понятия, существующие только в Flow и не имеющие встроенного эквивалента в TypeScript​

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

Синтаксис component​

В Flow предусмотрен специальный синтаксис component для объявления компонентов React. В TypeScript нет встроенного эквивалента: компоненты моделируются как обычные функции. Что даёт ключевое слово component по сравнению с обычной функцией:

  • Отдельные именованные параметры вместо объекта props. Не нужно дублировать деструктуризацию и описание типов, как в ({name}: {name: string}), или оборачивать props в Readonly<{...}>: параметры компонента по умолчанию доступны только для чтения.
  • Аннотация возвращаемого типа не требуется. Flow выводит тип React.Node и проверяет его, а также отклоняет компоненты с неявным возвращаемым значением.
  • Необязательная секция renders, ограничивающая допустимую JSX-структуру, которую может создавать компонент. Подробнее см. соответствующий раздел ниже.
  • Структурные правила обеспечиваются при разборе и проверке типов: не допускается this, вложенные объявления компонентов, а компоненты можно рендерить только как JSX, но не вызывать как обычные функции.
// Flow:
import * as React from 'react';


component Greeting(
  name: string,
  greeting: string = "Hello",
) {
  return <p>{greeting}, {name}!</p>;
}


component App() {
  return <Greeting name="World" />;
}

В TypeScript та же структура моделируется обычным функциональным компонентом и типом props. В отличие от component в Flow, полученное значение можно также вызывать как обычную функцию:

// TypeScript:
import * as React from 'react';

type GreetingProps = {
  name: string,
  greeting?: string,
};
function Greeting({
  name,
  greeting = "Hello",
}: GreetingProps) {
  return <p>{greeting}, {name}!</p>;
}

function App() {
  return <Greeting name="World" />;
}

const elem = Greeting({name: "World"}); // No error: TS treats components as callable

Вложенные объявления компонентов и вызовы в стиле функций приводят соответственно к [nested-component] и [react-rule-call-component]:

// Flow:
import * as React from 'react';


component Outer() {
  component Inner() { // ERROR: components may not be nested within other components or hooks
    return <div />;
  }
  return <Inner />;
}


const x = Outer({}); // ERROR: components cannot be called; use JSX (<Outer />) instead

Синтаксис hook​

Синтаксис hook — это специальное ключевое слово Flow для объявления хуков React. Flow использует это ключевое слово, чтобы на уровне типов обеспечивать соблюдение правил React в местах вызова хуков. В TypeScript аналога нет: соблюдение правил хуков в TS обеспечивается ESLint (eslint-plugin-react-hooks), который анализирует шаблоны AST без информации о типах и анализа всей программы.

Flow отслеживает принадлежность к хукам как часть типа функции, что позволяет выявлять нарушения, недоступные линтеру на основе AST. Ниже useToggle передаётся туда, где ожидается функция, не являющаяся хуком; Flow выдаёт ошибку [react-rule-hook-incompatible]. В месте вызова нет синтаксических признаков того, что fn будет вызван как хук внутри вызываемой функции, поэтому сопоставление шаблонов AST в ESLint не может обнаружить эту проблему:

// Flow:
import {useState} from 'react';


hook useToggle(
  initial: boolean,
): [boolean, () => void] {
  const [v, sv] = useState(initial);
  return [v, () => sv(x => !x)];
}


function callIt(
  fn: (boolean) => [boolean, () => void],
): [boolean, () => void] {
  return fn(false);
}


callIt(useToggle); // ERROR: `useToggle` is a hook; `fn` is not

В TypeScript нет понятия «хуковости»: useToggle — это просто функция. Приведённый ниже вызов проходит проверку типов, хотя во время выполнения callIt вызывает useToggle вне компонента или хука, и eslint-plugin-react-hooks тоже этого не обнаруживает: в месте вызова нет синтаксических признаков того, что fn будет вызван как хук внутри вызываемой функции:

// TypeScript:
import {useState} from 'react';

function useToggle(
  initial: boolean,
): [boolean, () => void] {
  const [v, sv] = useState(initial);
  return [v, () => sv(x => !x)];
}

function callIt(
  fn: (b: boolean) => [boolean, () => void],
): [boolean, () => void] {
  return fn(false);
}

callIt(useToggle); // No type error; rule-of-hooks violation surfaces at runtime

renders типы​

Типы рендеринга (renders, renders?, renders*) задают контракты композиции для компонентов: какие слоты они принимают и какие компоненты возвращают. Системы дизайна и библиотеки могут ограничивать композицию через компоненты-обёртки и HOC, а проверка типов будет отклонять нарушения. Встроенного аналога в TypeScript нет.

// Flow:
import * as React from 'react';


component Header(
  text: string,
  color: string,
) {
  return (
    <div style={{color}}>{text}</div>
  );
}
component MainHeader(
  text: string,
) renders Header {
  return (
    <Header text={text} color="red" />
  );
}


component Layout(
  header: renders Header,
) {
  return (
    <div>
      {header}
      <section>Content</section>
    </div>
  );
}


const ok = (
  <Layout
    header={<MainHeader text="Flow" />}
  />
);
const bad = <Layout header={<footer />} />; // ERROR: `<footer />` does not satisfy `renders Header`

В TypeScript нет аналога. Ближайший вариант — указать для слотов тип React.ReactNode, который принимает любой узел. Выразить контракт композиции невозможно, поэтому приведённый ниже случай <footer /> принимается так же, как предполагаемый вариант <MainHeader />:

// TypeScript:
import * as React from 'react';

function Header({
  text,
  color,
}: {text: string, color: string}) {
  return (
    <div style={{color}}>{text}</div>
  );
}
function MainHeader({text}: {
  text: string,
}) {
  return (
    <Header text={text} color="red" />
  );
}

function Layout({header}: {
  header: React.ReactNode,
}) {
  return (
    <div>
      {header}
      <section>Content</section>
    </div>
  );
}

const ok = (
  <Layout
    header={<MainHeader text="TS" />}
  />
);
const bad = (
  <Layout header={<footer />} />
); // No type error

match-выражения и инструкции​

В Flow есть match-выражения и инструкции для сопоставления с образцом со структурными образцами, защитными условиями и проверкой полноты. В TypeScript нет match; ближайший аналог для формы-инструкции — написанный вручную switch по дискриминируемому объединению с веткой assertNever. Форма-выражение вообще не имеет прямого аналога в TS, поскольку в JavaScript switch может использоваться только как инструкция. Пользователи TS обычно прибегают к вложенным тернарным операторам или IIFE с обёрнутым в неё switch, но оба варианта лишены структурных образцов, защитных условий и проверок полноты, которые предоставляет match.

// Flow:
type Action =
  | {type: 'add', text: string}
  | {type: 'toggle', id: string}
  | {type: 'remove', id: string}
  | {type: 'filter', mode: 'all' | 'active' | 'done'};


declare const action: Action;


const description: string = match (action) {
  {type: 'add', const text} =>
    `Add: ${text}`,
  {type: 'toggle', const id} =>
    `Toggle ${id}`,
  {type: 'remove', const id} =>
    `Remove ${id}`,
  {type: 'filter', mode: 'all'} =>
    'Show all',
  {type: 'filter', mode: 'active'} =>
    'Show active',
  {type: 'filter', mode: 'done'} =>
    'Show done',
};

В TypeScript нет match. Ближайший аналог формы-выражения — вложенные тернарные операторы, но структурные образцы, извлечение переменных из образца и проверки полноты при этом теряются:

// TypeScript:
type Action =
  | {type: 'add', text: string}
  | {type: 'toggle', id: string}
  | {type: 'remove', id: string}
  | {type: 'filter', mode: 'all' | 'active' | 'done'};

declare const action: Action;

const description: string =
  action.type === 'add' ? `Add: ${action.text}` :
  action.type === 'toggle' ? `Toggle ${action.id}` :
  action.type === 'remove' ? `Remove ${action.id}` :
  action.type === 'filter' && action.mode === 'all' ? 'Show all' :
  action.type === 'filter' && action.mode === 'active' ? 'Show active' :
  action.type === 'filter' && action.mode === 'done' ? 'Show done' :
  (() => { throw new Error('unreachable'); })();

Для match выполняется исчерпывающая проверка: пропуск случая — это [match-not-exhaustive], а в сообщении об ошибке указывается отсутствующий образец. Добавление нового типа действия или нового режима фильтра затем выявляет все места, где он не был обработан:

// Flow:
type Action =
  | {type: 'add', text: string}
  | {type: 'toggle', id: string}
  | {type: 'remove', id: string}
  | {type: 'filter', mode: 'all' | 'active' | 'done'};


declare const action: Action;


const description: string = match (action) { // ERROR: missing `{type: 'filter', mode: 'done'}`
  {type: 'add', const text} =>
    `Add: ${text}`,
  {type: 'toggle', const id} =>
    `Toggle ${id}`,
  {type: 'remove', const id} =>
    `Remove ${id}`,
  {type: 'filter', mode: 'all'} =>
    'Show all',
  {type: 'filter', mode: 'active'} =>
    'Show active',
};

Непрозрачные типы​

Непрозрачные псевдонимы типов скрывают базовый тип за пределами файла, в котором они определены, обеспечивая номинальную абстракцию на границах модулей. В TypeScript нет встроенного аналога; распространённый идиоматичный подход — «брендированные типы», создаваемые пересечением с маркирующим свойством (обычно с ключом unique symbol). Это пользовательский шаблон, а не возможность языка.

Граница, обеспечиваемая идиомой брендированных типов, слабее абстракции Flow, действующей в пределах файла: достаточно одного приведения as, чтобы получить брендированное значение. 42 as UserId проходит проверку типов в TypeScript, потому что источник (number) и целевой тип (number & {readonly [brand]: 'UserId'}) пересекаются по number, а TS отклоняет приведение as, только если типы по обе стороны не пересекаются.

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

// Flow:
opaque type UserId = number;


declare function makeUserId(
  n: number,
): UserId;
declare function lookupUser(
  id: UserId,
): string;


const id: UserId = makeUserId(42);
lookupUser(id); // OK
// In another file, `42` is not a `UserId` and a `UserId` is not a `number`.
// Inside this file (where the underlying type is visible) the conversion is allowed:
const n: number = id;

Взгляд из другого файла: за пределами определяющего модуля базовый тип запечатан. declare opaque type UserId (форма, которую видит импортирующий модуль) делает это ограничение наглядным: и структурное создание 42, и проекция обратно в number отклоняются:

// Flow (importer's view, as if `UserId` and functions were imported from another file):
declare opaque type UserId;
declare function makeUserId(
  n: number,
): UserId;
declare function lookupUser(
  id: UserId,
): string;


const id: UserId = makeUserId(42);
lookupUser(id); // OK
const forged: UserId = 42;    // ERROR: number is not a UserId
const n: number = id;         // ERROR: UserId is not a number

Кодирование брендированных типов в TypeScript с небезопасным приведением as в конце:

// TypeScript:
declare const brand: unique symbol;
type UserId = number & {
  readonly [brand]: 'UserId',
};

declare function makeUserId(
  n: number,
): UserId;
declare function lookupUser(
  id: UserId,
): string;

const id: UserId = makeUserId(42);
lookupUser(id); // OK

// `as` cast: source and target overlap on `number`, so TS accepts.
const forgedByCast = 42 as UserId;
lookupUser(forgedByCast);

Перечисления Flow​

Перечисления Flow и enum TypeScript на первый взгляд похожи, но в деталях сильно различаются.

Аспект TypeScript Flow
Исчерпывающая проверка switch Встроенного сообщения об ошибке нет; это реализуется с помощью never или линтера. Встроенная проверка: [invalid-exhaustive-check], если пропущен элемент.
Неявное преобразование в базовый примитив и обратно Допускает передачу числа в слоты перечислений с числовым базовым типом (кроме литералов, не совпадающих с элементами) и свободно преобразует перечисления в числа. Преобразования заблокированы в обоих направлениях; используйте .cast() для преобразования при передаче внутрь и value as <representation type> (например, as string / as number) для преобразования наружу.
Значения элементов по умолчанию Числовые перечисления автоматически нумеруются, начиная с 0. Элементам числовых перечислений необходимо явно задавать значения; строковые перечисления по умолчанию повторяют имена элементов.
Повторное объявление Допускается; значения по умолчанию могут незаметно конфликтовать. [name-already-bound].
Обратное сопоставление Для числовых перечислений во время выполнения создаётся обратная карта; для строковых перечислений такое обращение вызывает ошибку. .getName(value) работает и для числовых, и для строковых перечислений.
Перебор элементов Object.values(Status) — естественный способ перебрать значения, но для числового перечисления он возвращает и значения, и строки с именами элементов из обратной карты времени выполнения. Status.members() возвращает только значения.
Перечисления символов Нет. Поддерживаются (enum X of symbol { ... }).
Ограничения на определение Допускает неоднородные инициализаторы, инициализаторы не-литералы и имена элементов, начинающиеся со строчной буквы. Все три варианта вызывают ошибку.

У некоторых из этих особенностей есть причины, о которых полезно знать:

  • Правило значений по умолчанию существует потому, что добавление или удаление элемента в середине автоматически нумеруемого перечисления незаметно перенумеровывает все последующие элементы, что опасно при сериализации и ведении журналов.
  • Ошибка обратного сопоставления строковых перечислений TS имеет структурную причину: StatusStr.Off имеет литеральный тип "off" (значение), а не "Off" (ключ), поэтому StatusStr[StatusStr.Off] разрешается в несуществующий StatusStr["off"].
  • Object.values для числового перечисления из трёх элементов возвращает [ 'Active', 'Paused', 'Off', 0, 1, 2 ]: в результат попадают обе части обратной карты времени выполнения (а for...in для того же перечисления показывает такое же дублирование ключей).
  • Имена элементов, начинающиеся со строчной буквы, зарезервированы, поскольку перечисления Flow предоставляют методы со строчной буквы, например .cast и .members.

Проверка полноты встроена: пропуск случая в switch по перечислению Flow — это [invalid-exhaustive-check], при этом отсутствующий элемент указывается в сообщении. Добавление нового элемента затем выявляет все места, где он не был обработан:

// Flow:
enum Status {
  Active,
  Paused,
  Off,
}


declare const st: Status;


let label: string;
switch (st) { // ERROR: member `Off` has not been considered
  case Status.Active:
    label = 'on';
    break;
  case Status.Paused:
    label = 'wait';
    break;
}

Полное описание механики см. в документации по перечислениям Flow.

Односторонние проверки типов (implies)​

Функция-предикат с типом возвращаемого значения implies param is T уточняет тип параметра до T, только если функция возвращает true, и оставляет его без изменений, если функция возвращает false. Это способ обойти правило проверки тела функции, рассмотренное выше, когда верно только положительное направление. В TypeScript аналога нет.

Например, isPositive(n: ?number) возвращает true только для положительных чисел. Двустороннее n is number здесь было бы небезопасным: в ветке false может оказаться null, void или неположительное число, например 0 или -1, но ?number минус number оставляет только null | void, незаметно теряя неположительный случай. Форма implies указывает, что уточнение выполняется только в положительном направлении, поэтому в ветке else сохраняется исходный тип ?number.

// Flow:
function isPositive(
  n: ?number,
): implies n is number {
  return n != null && n > 0;
}


declare const n: ?number;
if (isPositive(n)) {
  n as number; // OK: refined to number
} else {
  n as ?number; // OK: stays ?number (not narrowed to null | void)
}

В TypeScript есть только двусторонние проверки типов. Ветка else всегда исключает тип предиката, даже если такое уточнение небезопасно. Приведённый ниже код проходит проверку типов, но выведенный тип ветки else неверен: во время выполнения n на самом деле может быть неположительным числом:

// TypeScript:
function isPositive(
  n: number | null | undefined,
): n is number {
  return n != null && n > 0;
}

declare const n:
  | number
  | null
  | undefined;
if (isPositive(n)) {
  // n: number
} else {
  // TS narrows n to `null | undefined` here, even though a non-positive
  // number (e.g. 0, -1) would also reach this branch at runtime.
}

import typeof​

import typeof — форма, доступная только в Flow. import type Foo from './m' (которую Flow использует совместно с TypeScript: она поддерживается обоими языками) импортирует тип из экспорта типа; import typeof Foo from './m' — специфичная для Flow форма, импортирующая тип из экспорта значения, чтобы использовать его в аннотации типа.

Ближайший аналог import typeof в TypeScript — typeof import('./m'), но форма привязки отличается: TypeScript создаёт форму пространства имён и обычно используется с индексированным доступом (typeof import('./m')['Foo']), тогда как import typeof Foo from './m' привязывает тип одного значения к имени типа верхнего уровня.

В примере ниже используются обе формы: import type {Node} напрямую импортирует экспорт типа для аннотации типа, а import typeof {useState} импортирует тип экспорта значения. Тип обобщённого значения сохраняет возможность параметризации через import typeof: можно записать useState<number> для импортированного типа, специализировав импортированный тип функции для number, чтобы параметр хука можно было вызвать как useState с типом number.

// Flow:
import type {
  Node as ReactNode,
} from 'react';
const node: ReactNode = "hello, world";


import typeof {useState} from 'react';
hook useCounter(
  useStateNum: useState<number>,
): number {
  const [count, setCount] =
    useStateNum(0);
  setCount(c => c + 1);
  return count;
}

Вспомогательные типы только для Flow​

Некоторые вспомогательные типы не имеют аналога в TypeScript. Ближайшие варианты записи в TS (если они есть) приведены ниже; остальные не имеют встроенной формы в TS и обычно реализуются с помощью пользовательских шаблонов.

  • Class<T> — тип конструктора класса для типа экземпляра T. Встроенной формы в TS нет; обычно в TS используют new (...args: any[]) => T или typeof T для конкретного класса.
  • Values<T> — объединение типов значений свойств T. В TS ему соответствует индексированный доступ T[keyof T].

Синтаксические формы только для Flow​

Некоторые формы аннотаций типов Flow не имеют соответствующей записи в TypeScript: tsc отклоняет их при разборе. Это альтернативный синтаксис для существующих концепций Flow.

  • Встроенная аннотация типа interface — type T = interface { foo: number }. Позволяет использовать интерфейс внутри выражения типа, а не только в объявлении верхнего уровня. В TypeScript требуется отдельная инструкция interface I { ... }.
  • Тип необязательного индексированного доступа — Obj?.['prop'] на уровне типов повторяет оператор времени выполнения ?.: если Obj равен null или undefined, результатом будет void, иначе — Obj['prop']. На уровне типов в TypeScript нет ?..
  • Параметры анонимного типа функции — type F = string => void. В Flow можно не указывать имя параметра, если оно не несёт информации; TypeScript требует (x: string) => void.
  • Анонимные параметры индексатора — type O = {[string]: number}. Аналогично: Flow позволяет не указывать имя ключа индекса, если оно нигде не используется; TypeScript требует {[k: string]: number}.
// Flow:
type Inline = interface { foo: number };
type Opt = ?{foo: number};
type Pulled = Opt?.['foo']; // number | void
type Fn = string => void;
type Dict = {[string]: number};

Интеграция с Relay / GraphQL​

Настройка relay_integration=true в [options] позволяет Flow изначально понимать помеченные тегом шаблонные строки graphql и выводить их типы по артефактам, созданным компилятором Relay. Благодаря этому можно не указывать явные параметры типа в useFragment, usePreloadedQuery и т. д. Сопутствующие параметры: relay_integration.esmodules (разрешать артефакты как экспорты ES-модулей по умолчанию, а не CommonJS) и relay_integration.excludes (отключение для отдельных каталогов). См. документацию по этому параметру.

В TypeScript нет аналога на уровне средства проверки типов. Пользователи TypeScript либо явно передают сгенерированный тип (useFragment<MyFragment$key>(...)), либо используют плагин языкового сервиса TypeScript для подсказок в редакторе (без проверки типов), либо применяют шаблоны узлов документа, например graphql-typed-document-node / gql.tada, требующие явного импорта сгенерированных типов.

С relay_integration=true Flow читает артефакт компилятора Relay, созданный для MyFragment, и выводит типы ключа и результата из тега graphql — без параметра типа и без импорта сгенерированного типа:

// Flow:
import {
  graphql,
  useFragment,
} from 'react-relay';
declare const userRef: MyFragment$key;

const data = useFragment(
  graphql`fragment MyFragment on User { name }`,
  userRef,
);

В TypeScript требуется явно указать параметр типа и импортировать сгенерированный тип:

// TypeScript:
import {
  graphql,
  useFragment,
} from 'react-relay';
import type {
  MyFragment$key,
} from './__generated__/MyFragment.graphql';
declare const userRef: MyFragment$key;

const data = useFragment<MyFragment$key>(
  graphql`fragment MyFragment on User { name }`,
  userRef,
);

Возможности только TypeScript, отсутствующие в Flow​

Это возможности TypeScript, у которых сегодня нет аналога в Flow. Некоторые из них Flow намеренно не поддерживает: они либо пересекаются с возможностями Flow, но имеют другие (обычно менее консервативные) значения по умолчанию, либо создают подводные камни, которых Flow избегает. Другие возможности пока просто не реализованы (см. отдельный раздел «Скоро появится» о возможностях в разработке или ожидающих разрешения на выпуск). Использовать любые перечисленные ниже возможности в коде Flow не получится; в некоторых случаях синтаксис TypeScript будет разобран, и ошибка проявится позже, чем ожидается.

Синтаксические формы только для TS​

Некоторые формы поверхностного синтаксиса TS не имеют соответствующей записи в Flow, но сама концепция доступна в Flow под другим названием. Flow отклоняет форму TS во время разбора или проверки типов, указывая в сообщении об ошибке непосредственно на вариант переписывания для Flow.

  • Утверждение типа в угловых скобках — TS <T>x → Flow x as T.
  • Необязательные безымянные элементы кортежа — TS [number, string?] → Flow [a: number, b?: string]. Для необязательных элементов Flow требует вариант с метками.
  • Оператор типа readonly для кортежей — TS readonly [T, S] → Flow Readonly<[T, S]>.
  • Оператор типа readonly для краткой записи массива — TS readonly T[] → Flow ReadonlyArray<T>.

Обратите внимание: readonly в качестве модификатора свойства ({readonly x: T}) и для параметров типа (out T) работает одинаково в обоих языках — см. раздел Ключевые слова ковариантности и контравариантности. Две приведённые выше формы readonly используют readonly как оператор типа (префикс структурного типа); такая запись есть только в TS, а в Flow вместо неё используется вспомогательный тип-обёртка.

Декораторы​

Flow разбирает синтаксис декораторов, но не проверяет их типы: тип декоратора никогда не применяется к исходному значению, а сам декоратор незаметно удаляется. TypeScript поддерживает два несовместимых режима: декораторы TC39 (по умолчанию, с параметром-объектом контекста) и устаревшие декораторы (при --experimentalDecorators, со старой сигнатурой (target, key)).

Расширения синтаксиса классов TypeScript​

В TypeScript есть несколько расширений синтаксиса классов, которые Flow намеренно не поддерживает, предлагая вместо них писать соответствующий код JavaScript.

  • Свойства параметров (constructor(public x: number)) — сокращённая запись только для TS, которая генерирует код времени выполнения: автоматически объявляет поле и присваивает ему значение аргумента конструктора. Сообщение Flow: «Flow не поддерживает свойства параметров TypeScript. Чтобы исправить ошибку, объявите свойство в теле класса и присвойте ему значение в конструкторе».

  • Модификаторы доступа public / protected / private — проверяемое TS управление доступом. В TypeScript они влияют только на проверку типов (во время выполнения поле всё равно общедоступно), поэтому их удаление безопасно. Flow отклоняет все три модификатора; удалите их (public / protected не влияют на выполнение) или замените private foo на #foo. Вариант с #private приводит к иной форме во время выполнения, чем форма TS: поля ECMAScript #private номинально являются приватными во время выполнения, тогда как private в TS удаляется.

  • Автоматические аксессоры accessor (class C { accessor x: T = init }) — предложение TC39, преобразуемое в пару геттера и сеттера с приватным полем в качестве основы. Flow не разбирает эту форму. Напишите геттер и сеттер явно, используя поле-основу #private, или используйте обычное поле, если обёртка-аксессор не нужна.

Блоки времени выполнения namespace​

Блоки namespace { ... } на уровне исходного кода не поддерживаются. В Flow есть declare namespace для объявлений окружения внутри libdefs, но нет блоков пространств имён на уровне исходного кода, создающих значения времени выполнения.

const enum​

Аналога нет. Преобразования Flow создают для перечислений Flow объект времени выполнения, а не подставляют их непосредственно в местах использования, как это делает tsc для const enum. Ограничения этих перечислений (только литеральные значения, запрет повторного объявления, отсутствие числовых значений по умолчанию) всё же упрощают встраивание на этапе сборки, если оно требуется.

Функции утверждения​

Тип возвращаемого значения asserts x is T в TypeScript объявляет функцию, которая выбрасывает исключение при неудачном утверждении и безусловно уточняет параметр до T после возврата из вызова. Это отличается от проверки типа, которая возвращает логическое значение и уточняет тип только внутри if/else. В Flow есть проверки типов (x is T), но нет формы asserts x is T.

Ближайший аналог в Flow — проверка типа в сочетании с явным throw в месте вызова: function isStr(x: unknown): x is string { ... }, а затем if (!isStr(x)) throw new Error();.

Вспомогательный тип ThisType<T>​

В TypeScript ThisType<T> — это маркер, используемый внутри контекстного типа для переназначения this на T в методах литерала объекта. Flow не реализует такое переназначение. В Flow отсутствуют два различия между языками, благодаря которым ThisType<T> полезен в TypeScript:

  • Литералы объектов напрямую отклоняют ссылки this, поэтому нет тела метода литерала объекта, чей this Flow мог бы переназначить.
  • У методов классов и интерфейсов есть фиксированная привязка this, связанная с объявляющим их типом; внешний маркер не может изменить эту привязку.

Выражения с аргументами типа​

TypeScript допускает аргументы типа у выражения-значения: отдельное выражение Foo<string> специализирует обобщённый тип, после чего его можно привязать к имени. Flow не разбирает эту форму и выдаёт ошибку ParseError вскоре после закрывающей >.

// TypeScript:
declare class Foo<T> {
  value: T;
}
const StringFoo = Foo<string>;

В Flow вместо объявления предварительно специализированной привязки нужно указать аргументы типа при создании экземпляра или в месте вызова (new Foo<string>(), f<string>(x)).

Уточнение по маркеру для деструктурированных значений​

TypeScript совместно уточняет деструктурированные свойства дискриминируемого объединения: уточнение привязки маркера также уточняет другие привязки, полученные при той же деструктуризации.

// TypeScript:
type FormField =
  | {kind: 'text', value: string}
  | {kind: 'number', value: number};

declare const field: FormField;
const {kind, value} = field;
if (kind === 'text') {
  const s: string = value; // OK - TS narrows `value` based on `kind`
} else {
  const n: number = value;
}

Flow уточняет объединения с маркером через исходное значение (if (field.kind === 'text') { ... field.value ... }), но каждая деструктурированная привязка сохраняет полный тип объединения независимо от остальных, поэтому тот же код не проходит проверку:

// Flow:
type FormField =
  | {kind: 'text', value: string}
  | {kind: 'number', value: number};


declare const field: FormField;
const {kind, value} = field;
if (kind === 'text') {
  const s: string = value; // ERROR: `value` keeps its full `string | number` type
}

В Flow вместо деструктуризации следует выполнять уточнение через исходное значение. Другой вариант — использовать match-выражение или инструкцию, где сопоставление и деструктуризация объединены: каждая ветвь сопоставляет маркер и привязывает value с типом, соответствующим этой ветви:

// Flow:
type FormField =
  | {kind: 'text', value: string}
  | {kind: 'number', value: number};


declare const field: FormField;
match (field) {
  {kind: 'text', const value} => {
    const s: string = value;
  }
  {kind: 'number', const value} => {
    const n: number = value;
  }
}

Расширение модулей на стороне пользователя​

На уровне исходного кода аналога нет. Пользователи TypeScript часто повторно открывают сторонние модули из исходного кода с помощью declare module 'name' { ... }, чтобы добавить типы. declare module в Flow используется только внутри libdefs в flow-typed/, но не в произвольных исходных файлах.

Скоро появится​

Для следующих возможностей TypeScript в Flow уже реализована поддержка проверки типов, но для снятия ограничения на выпуск Flow ожидает обновления инструментов (например, Prettier). Они доступны в песочнице Flow.

  • Типы шаблонных строк, например `${'a' | 'b'}-${'x' | 'y'}`.
  • Модификаторы отображаемых типов — удаление вариативности -readonly и переименование ключей as.
  • Абстрактные классы и методы.
  • override для членов класса.
  • Типы конструкторов — type Ctor = new (x: number) => R.
  • Выражение satisfies — проверяет выражение на соответствие типу, не расширяя выведенный тип.
  • Встроенное выражение типа import() — type A = import('./m').A.
  • import X = require('foo') и export = X — привязки импорта и экспорта в стиле CommonJS.

Планируется:

  • Поддержка обращений к свойствам с ключами-символами на уровне типов. Синтаксис unique symbol уже разбирается, но система типов пока не моделирует ключи-символы как отдельные номинальные ключи.

Сближение синтаксиса с TypeScript​

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

Устаревшая форма Flow Современная форма Flow (согласованная с TS)
mixed unknown
$Keys<T> keyof T
$ReadOnly<T> Readonly<T>
$NonMaybeType<T> NonNullable<T>
$ReadOnlyArray<T> ReadonlyArray<T>
<T: Bound> <T extends Bound>
приведение (x: T) x as T
{| a: number |} exact {a: number} (exact используется по умолчанию)
вариативность свойств +foo / -foo readonly foo / writeonly foo (writeonly используется только в Flow)
вариативность параметров типа +T / -T out T / in T
функции-предикаты %checks определяемые пользователем проверки типов (function isString(x: unknown): x is string)
$ObjMap<O, F> / $ObjMapi<O, F> / $TupleMap<T, F> / $TupleMapi<T, F> отображаемые типы ({[K in keyof O]: ...}) со встроенным телом функции
$PropertyType<T, K> / $ElementType<T, K> индексированный доступ (T[K])
$Call<F, ...Args> ReturnType<F> вместе с индексированным доступом либо условный тип с infer
$Diff<A, B> / $Rest<A, B> обычно Omit<A, keyof B>; зависит от конкретного случая (семантика не всегда идентична)

Полное описание см. в разделе «Модернизация устаревшего синтаксиса Flow».

Параметры конфигурации, согласованные с TypeScript​

Некоторым параметрам-переключателям Flow [options] из раздела .flowconfig напрямую соответствуют флаги строгости compilerOptions в TypeScript: семантика та же, но значения по умолчанию различаются. В используемой на этой странице конфигурации TypeScript strict параметр useUnknownInCatchVariables включён через strict, тогда как noUncheckedIndexedAccess не входит в strict и остаётся необязательным. В Flow нет общего набора параметров strict: оба флага настраиваются отдельно и по умолчанию имеют значение false. Поэтому при переносе проекта TS с включённым strict нужно включить use_unknown_in_catch_variables, чтобы настройки соответствовали друг другу.

Параметр TypeScript Параметр Flow Описание
noUncheckedIndexedAccess no_unchecked_indexed_access При индексированном доступе через массив или словарь тип результата расширяется за счёт undefined (в Flow: void), поэтому чтение arr[i] или dict[k] возвращает T | void вместо T и заставляет вызывающий код сузить тип перед использованием. Доступ к кортежу по литеральному индексу в обоих языках не меняется. Подробнее см. в документации.
useUnknownInCatchVariables use_unknown_in_catch_variables Изменяет тип по умолчанию для неаннотированной привязки catch с any на unknown. Перед использованием вызывающий код должен сузить тип значения (instanceof Error, typeof e === 'string', …). Подробнее см. в документации.

Типизация внешнего кода​

Файлы .d.ts в TypeScript решают две разные задачи: типизацию сторонних npm-пакетов и типизацию собственного (или поставляемого вместе с проектом) кода, который должен оставаться обычным JavaScript. В Flow для этих задач предусмотрены два механизма.

Сценарий использования в TypeScript Механизм Flow Расположение и разрешение
Объявления сторонних пакетов, включая пакеты @types/* и файлы .d.ts на уровне пакета. Определения библиотек (libdefs). Обычные файлы .js в flow-typed/, в которых пакеты обычно указываются с помощью declare module 'pkg' { ... }.
Файл объявлений, расположенный рядом с реализацией на JavaScript. Файлы объявлений. Файлы .js.flow или .json.flow, расположенные рядом с реализацией (например, Misc.js.flow рядом с Misc.js) и подменяющие файл реализации.
Дополнение модуля на уровне исходного кода с помощью declare module 'pkg' { ... } из произвольных файлов проекта. Эквивалента на уровне исходного кода нет. Форма declare module 'pkg' { ... } в Flow предназначена для libdefs в flow-typed/, а не для повторного открытия модулей из обычных исходных файлов или расположенных рядом файлов объявлений .js.flow.

В обоих механизмах Flow используется значительная часть синтаксиса ambient-объявлений TypeScript, но расположение имеет принципиальное значение. declare class, declare function, declare const и родственные формы могут описывать ambient-значения в libdefs, файлах объявлений или встроенных объявлениях; declare module 'name' { ... } — это форма с указанием имени пакета, используемая в libdefs. Файлы объявлений обычно описывают непосредственно экспорты расположенного рядом модуля, например с помощью declare export ... или declare module.exports.

См. раздел Дополнение модулей на стороне пользователя, посвящённый шаблону с файлами объявлений, который поддерживается в TypeScript, но не в Flow.

Частичная поддержка слияния объявлений​

Flow, как и TypeScript, использует модель разделённых пространств имён и поддерживает подмножество слияния объявлений. Каждое имя независимо существует в пространстве имён значений и пространстве имён типов, поэтому один идентификатор может одновременно быть значением и типом без конфликта: const A = 1; interface A {} допустимо; при использовании A в контексте значения выполняется обращение к константе, а в контексте типа — к интерфейсу. Конструкции, используемые в обоих пространствах имён (классы, перечисления), регистрируются один раз в пространстве имён значений, а в пространстве типов используется резервный поиск по нему.

Что объединяет Flow:

  • interface + interface — объединяются члены (совместимые дубликаты разрешены; конфликтующие члены вызывают ошибку). В файлах определений библиотек списки extends также объединяются, сигнатуры вызовов перегружаются как пересечения, а несовпадение арности параметров типа вызывает ошибку. В обычных исходных файлах слияние ограничено членами; списки extends и сигнатуры вызовов не объединяются в разных объявлениях, а одноимённые интерфейсы нельзя экспортировать из одного файла одновременно.
  • declare class + interface — члены интерфейса включаются в класс (в любом порядке).
  • function / declare function + declare namespace — члены типа из пространства имён включаются в функцию (в любом порядке) и доступны как fn.T.
  • class / declare class + declare namespace — члены типа из пространства имён включаются в класс (в любом порядке) и доступны как Cls.T.

Чего Flow не делает:

  • Слияние во время выполнения. Только члены типа из declare namespace надёжно распространяются на соседнюю функцию или класс; члены-значения не рассматриваются как свойства хоста во время выполнения. Слияние на стороне значений в стиле TypeScript function + namespace, при котором пространство имён добавляет члены времени выполнения, не поддерживается.
  • Слияние нескольких блоков declare module. Несколько блоков declare module 'name' { ... } для одного модуля не объединяются: второй считается переопределением первого. Для каждого модуля должен быть только один libdef.
  • Дополнение declare module 'name' { ... } на стороне пользователя на уровне исходного кода (см. раздел Дополнение модулей на стороне пользователя).

Генерация файлов объявлений​

Описанные выше механизмы предназначены для объявлений как входных данных: они позволяют типизировать код, который иначе недоступен для проверки типов. Обратная задача — создание файлов объявлений из исходного кода. В TypeScript это встроено в компилятор: tsc --declaration создаёт .d.ts для каждого скомпилированного .ts, а --emitDeclarationOnly генерирует объявления без соответствующего .js. В Flow такая возможность не встроена в исполняемый файл flow; этот пробел заполняет отдельный npm-пакет flow-api-translator, создающий файлы .js.flow или .d.ts из исходного файла Flow.

См. также​

  • Глоссарий — содержит краткое примечание о соответствующем понятии в TypeScript и служит удобным указателем, если нужно найти только один термин.
  • Модернизация устаревшего синтаксиса Flow — полный справочник по переносу устаревших утилит Flow с префиксом $ и других старых синтаксических форм на современные аналоги (часто соответствующие TypeScript).

© 2013–present Facebook Inc.
Licensed under the MIT License.
https://flow.org/en/docs/flow-vs-typescript

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API