Требования к аннотациям
Flow использует локальный вывод типов в качестве алгоритма вывода. Правила в этом разделе отражают основные особенности проектирования этой схемы вывода.
Flow старается не требовать аннотаций типов для частей программ, где типы можно легко вывести из непосредственного контекста выражения, переменной, параметра и т. д.
TypeScript по умолчанию выводит типы через границы модулей, тогда как Flow требует аннотации на границе (параметры функций, экспорты), чтобы обеспечить параллельные проверки. В TypeScript есть эквивалентная возможность, которую можно включить (--isolatedDeclarations, TS 5.5+), но она не входит в strict и по умолчанию отключена. Полное сравнение см. в разделе На границах модулей требуются аннотации.
Объявления переменных
Рассмотрим, например, следующее определение переменной
const len = "abc".length;
Вся информация, необходимая для вывода типа len, содержится в инициализаторе "abc".length. Сначала Flow определит, что "abc" — это строка, а затем — что свойство length строки является числом.
Ту же логику можно применить ко всем инициализациям вида const. Ситуация немного усложняется, когда инициализация переменной охватывает несколько инструкций, например:
declare const maybeString: ?string;
let len;
if (typeof maybeString === "string") {
len = maybeString.length;
} else {
len = 0;
} Flow всё ещё может определить, что len — это number, но для этого ему приходится просматривать несколько инструкций-инициализаторов. Подробные сведения о том, как различные шаблоны инициализации определяют тип переменной и когда необходимо аннотировать объявление переменной, см. в разделе объявления переменных.
Параметры функций
В отличие от объявлений переменных, для определения типов параметров функций нельзя использовать подобное рассуждение с «просмотром вперёд». Рассмотрим функцию
function getLength(x) {
return x.length;
} Существует много видов x, у которых можно получить и вернуть свойство length: например, объект со свойством length или строка. Если бы далее в программе мы вызвали getLength следующим образом
getLength("abc");
getLength({length: 1}); одним из возможных выводов было бы, что x — это string | { length: number }. Однако это означает, что тип getLength определяется любой частью текущей программы. Такие глобальные рассуждения могут привести к неожиданным эффектам удалённого воздействия, поэтому их стараются избегать. Вместо этого Flow требует аннотировать параметры функций. Отсутствие такой аннотации типа проявляется в виде ошибки [missing-local-annot] для параметра x, а тело функции проверяется с типом x: any:
function getLength(x) { // Error
return x.length;
}
const n = getLength(1); // no error since getLength's parameter type is 'any' Чтобы исправить эту ошибку, достаточно аннотировать x следующим образом:
function getLength(x: string) {
return x.length;
} Это же требование действует и для методов классов
class WrappedString {
data: string;
setStringNoAnnotation(x) { // Error
this.data = x;
}
setString(x: string) {
this.data = x;
}
} Контекстная типизация
Параметры функций не всегда нужно аннотировать явно. В случае функции обратного вызова, передаваемой при вызове функции, тип параметра можно легко вывести из непосредственного контекста. Рассмотрим, например, следующий код
const arr = [0, 1, 2]; const arrPlusOne = arr.find(x => x % 2 === 1);
Flow выводит, что тип arr — это Array<number>. Сопоставив это со встроенной информацией о Array.find, Flow может определить, что типом x => x % 2 === 1 должен быть number => unknown. Этот тип служит для Flow подсказкой и предоставляет достаточно информации, чтобы определить тип x как number.
Любая сопутствующая аннотация потенциально может служить подсказкой для параметра функции, например:
const fn1: (x: number) => number = x => x + 1;
Однако возможно и такое, что аннотацию нельзя использовать в качестве подсказки для параметра функции:
const fn2: unknown = x => x + 1; // Error
В этом примере тип unknown просто не содержит достаточно информации, чтобы извлечь возможный тип для x.
Flow может выводить типы неаннотированных параметров, даже если они вложены в другие выражения, например в объекты. Например:
const fn3: {f: (number) => void} = {f: (x) => {x as string}}; // Error Flow выведет number как тип x, поэтому приведение типа завершится ошибкой.
Типы возвращаемых значений функций
В отличие от параметров функций, тип возвращаемого значения функции обычно не требует аннотации. Поэтому приведённое выше определение getLength не вызовет ошибок Flow.
Однако из этого правила есть несколько важных исключений. Первое — методы классов. Если добавить в класс WrappedString метод getString, возвращающий внутреннее свойство data:
class WrappedString {
data: string;
getString(x: string) { // Error
return this.data;
}
} Flow сообщит, что для возвращаемого значения getString отсутствует аннотация.
Второе исключение — рекурсивные определения. Простой пример:
function foo() { // Error
return bar();
}
function bar() {
return foo();
} Приведённый выше код вызывает ошибку [definition-cycle], указывающую на два места, образующих цикл зависимостей: два отсутствующих типа возвращаемых значений. Добавление аннотации возвращаемого значения к любой из функций решит эту проблему.
По сути, требование аннотации возвращаемых значений методов — это частный случай ограничения на рекурсивные определения. Рекурсия возможна через обращение к this.
Вызовы обобщённых функций
При вызове обобщённых функций тип результата может зависеть от типов значений, переданных в качестве аргументов. В этом разделе описано, как вычисляется результат, если аргументы типа не указаны явно.
Рассмотрим, например, определение map и возможный вызов с аргументами x => x + 1 и [1, 2, 3]:
declare function map<T, U>( f: (T) => U, array: ReadonlyArray<T>, ): Array<U>; map(x => x + 1, [1, 2, 3]);
Здесь Flow выводит, что тип x — это number.
К другим распространённым примерам вызовов обобщённых функций относятся вызов конструктора обобщённого класса Set или вызов useState из библиотеки React:
import {useState} from 'react';
const set = new Set([1, 2, 3]);
component Example() {
const [num, setNum] = useState(42);
return null;
} В этом случае Flow выводит, что тип set — это Set<number>, а num и setNum имеют типы number и (number) => void соответственно.
Вычисление решения
Вычисление результата вызова обобщённой функции сводится к следующему:
- найти решение для
TиU, не содержащее обобщённых частей; - заменить
TиUэтим решением в сигнатуреmap; - выполнить вызов с новой сигнатурой
map.
Этот процесс разработан с учётом двух целей:
- Корректность. Результаты должны обеспечить корректность вызова на третьем шаге.
- Полнота. Типы, создаваемые Flow, должны быть как можно более точными и информативными, чтобы проверка остальных частей программы завершилась успешно.
Посмотрим, как эти две цели проявляются на приведённом выше примере с map.
Flow обнаруживает, что ReadonlyArray<T> должен быть совместим с типом [1, 2, 3]. Поэтому он может вывести, что T — это number.
Зная T, теперь можно успешно проверить x => x + 1. Параметр x получает контекстный тип number, поэтому результат x + 1 также является числом. Это последнее ограничение позволяет вычислить, что U тоже имеет тип number.
Новая сигнатура map после замены обобщённых частей приведённым выше решением выглядит так:
(f: (number) => number, array: ReadonlyArray<number>) => Array<number>
Нетрудно убедиться, что вызов будет успешно проверен.
Ошибки при полиморфных вызовах
Если описанный выше процесс проходит без сбоев, ошибок, связанных с вызовом, быть не должно. Но что происходит, если процесс завершается неудачей?
Есть две причины, по которым процесс может завершиться неудачей:
Недостаточно ограниченные параметры типа
В некоторых случаях Flow может не располагать достаточной информацией, чтобы определить тип параметра типа. Ещё раз рассмотрим вызов конструктора встроенного обобщённого класса Set, на этот раз без аргументов:
const set = new Set(); // Error
set.add("abc"); При вызове new Set мы не предоставляем Flow достаточно информации для определения типа T, хотя следующий вызов set.add явно подразумевает, что T будет строкой. Помните, что вывод аргументов типа выполняется локально для вызова, поэтому Flow не будет просматривать последующие инструкции, чтобы определить тип.
При отсутствии информации Flow мог бы вывести любой тип для T: any, mixed, empty и т. д. Такое решение нежелательно, поскольку оно может привести к неожиданным результатам. Например, если бы мы молча выбрали Set<empty>, вызов set.add("abc") завершился бы ошибкой несовместимости между string и empty, при этом было бы неясно, откуда взялся empty.
Поэтому в подобных ситуациях вы получите ошибку [underconstrained-implicit-instantiation]. Чтобы исправить её, добавьте аннотацию типа. Это можно сделать несколькими способами:
-
Добавьте аннотацию в месте вызова одним из двух способов:
- укажите явный аргумент типа
const set = new Set<string>();
; - добавьте аннотацию к переменной инициализации:
const set: Set<string> = new Set();
;
- укажите явный аргумент типа
-
Укажите тип по умолчанию для параметра типа
Tв определении класса:declare class SetWithDefault<T = string> extends ReadonlySet<T> { constructor(iterable?: ?Iterable<T>): void; // more methods ... }Если в месте вызова нет информации о типе, Flow использует тип по умолчанию для
Tв качестве выведенного аргумента типа:const defaultSet = new SetWithDefault(); // defaultSet is SetWithDefault<string>
Ошибки несовместимости
Даже если Flow удаётся вывести необобщённые типы для параметров типа в обобщённом вызове, эти типы всё равно могут привести к несовместимости как в текущем вызове, так и в последующем коде.
Например, если бы мы выполнили следующий вызов map:
declare function map<T, U>(f: (T) => U, array: ReadonlyArray<T>): Array<U>;
map(x => x + 1, [{}]); // Error Flow выведет T как {}, а значит, присвоит x тип {}. Это приведёт к ошибке при проверке стрелочной функции, поскольку операция + недопустима для объектов.
Наконец, распространённый источник ошибок — случай, когда выведенный тип в обобщённом вызове подходит для самого вызова, но не соответствует предполагаемому дальнейшему использованию в коде. Например:
import {useState} from 'react';
component Example() {
const [str, setStr] = useState("");
declare const maybeString: ?string;
setStr(maybeString); // Error
return null;
} Передача строки "" в вызов useState заставляет Flow вывести string как тип состояния. Поэтому при последующем вызове setStr также будет ожидать на вход string, и передача ?string приведёт к ошибке.
И снова для исправления ошибки достаточно аннотировать ожидаемый «более общий» тип состояния при вызове useState:
import {useState} from 'react';
component Example() {
const [str, setStr] = useState<?string>("");
return null;
} Экспорты модулей
Flow формирует сигнатуры типов для каждого модуля исключительно на основе его экспортов, не анализируя внутреннюю реализацию модуля. Этот процесс называется извлечением сигнатуры: Flow создаёт для каждого модуля «типизированный интерфейс» — сводку типов всех экспортируемых значений, анализируя только место экспорта, но не тело модуля. Это позволяет Flow проверять типы модулей параллельно и быстро предоставлять обратную связь в IDE: тип каждого модуля можно понять без проверки реализаций его зависимостей.
Например, для экспортируемой функции нужны аннотации параметров и возвращаемого значения:
// Works: Flow can build a signature from the annotations
export function getLength(x: string): number {
return x.length;
}
export function getLength2(x) { // Error: Flow cannot determine the parameter or return type
return x.length;
} Экспортируемые переменные обычно не требуют аннотаций, если их тип можно определить по инициализатору:
declare const items: Array<string>; export const name = "Alice"; // OK: type is clearly 'string' export const count = items.length; // OK: type determined from .length
Если Flow не может определить тип экспорта, вы увидите ошибку [signature-verification-failure] с сообщением «Невозможно построить типизированный интерфейс для этого модуля». Чтобы исправить ошибку, добавьте аннотацию типа к экспорту.
Неподдерживаемые формы выражений в экспортах
Помимо отсутствующих аннотаций, некоторые формы выражений не поддерживаются в экспортируемых позициях, поскольку Flow не может определить их типы при извлечении сигнатуры. Обычно они вызывают ошибку [signature-verification-failure] с конкретным пояснением:
-
Непредвиденный ключ объекта — в экспортируемых объектах должны использоваться простые литеральные ключи (строковые или числовые литералы, идентификаторы). Вычисляемые ключи не позволяют Flow определить структуру объекта в месте экспорта.
const key = "x"; export const obj = {[key]: 1}; // Error: Expected simple key in objectИсправление: аннотируйте экспорт, например
export const obj: {x: number} = {[key]: 1}. -
Непредвиденное расширение массива — расширение при создании экспортируемого литерала массива не поддерживается, поскольку Flow пришлось бы вычислять результат расширения, чтобы определить тип элементов массива.
const xs = [1, 2]; export const ys = [...xs, 3]; // Error: Unexpected spread in array
Исправление: добавьте аннотацию типа, например
export const ys: Array<number> = [...xs, 3]. -
Пропуск элемента массива — экспортируемые массивы с пропущенными элементами (элисиями, такими как
[1, , 3]) не поддерживаются.export const arr = [1, , 3]; // Error: Unexpected array hole
Исправление: заполните пропуск или аннотируйте экспорт, например
export const arr: Array<number | void> = [1, , 3]. -
Пустой массив — у пустого массива
[]в позиции экспорта нет элементов, по которым Flow мог бы вывести тип элементов.export const items = []; // Error
Исправление: добавьте аннотацию типа, например
export const items: Array<string> = []. -
Неподдерживаемое выражение — некоторые сложные выражения (например, условные выражения, вызовы функций и логические выражения) невозможно вычислить при извлечении сигнатуры.
declare const condition: boolean; export const value = condition ? 1 : "a"; // Error
Исправление: добавьте аннотацию типа, например
export const value: number | string = condition ? 1 : "a".
Обобщённый контекст
Если переменной, объявленной во внешней области видимости, присваивают значение только внутри обобщённой функции, Flow не может безопасно вывести её тип. Присваивания выполняются в «обобщённом контексте» — они включают параметры типа, которые при каждом вызове могут принимать любой конкретный тип. Поскольку переменная находится вне обобщённой функции, Flow должен знать её тип независимо от конкретного вызова.
Вы увидите ошибку [invalid-declaration] примерно с таким сообщением: «Для переменной X необходимо указать аннотацию, поскольку она инициализируется только в обобщённом контексте».
let lastSeen; // Error: only initialized in a generic context
function remember<T>(value: T): T {
lastSeen = value;
return value;
}
console.log(lastSeen); Чтобы исправить ошибку, добавьте аннотацию типа к переменной:
let lastSeen: unknown; // Works!
function remember<T>(value: T): T {
lastSeen = value;
return value;
}
console.log(lastSeen); Это также относится к переменным, инициализированным значением null, которым затем присваивают значения только в обобщённом контексте. В этом случае вы увидите сообщение: «Для переменной X необходимо указать аннотацию, поскольку ей присваивается только null и в обобщённом контексте».
let cached = null; // Error: only assigned by null and in generic context
function cache<T>(value: T): T {
cached = value; // Error: T is incompatible with null
return value;
}
console.log(cached); Как и в предыдущем примере, для исправления нужно аннотировать переменную:
let cached: unknown = null; // Works!
function cache<T>(value: T): T {
cached = value;
return value;
}
console.log(cached); Литералы пустых массивов
В Flow литералы пустых массивов ([]) обрабатываются особым образом. Подробнее об их поведении и требованиях.
См. также
- Переменные — как объявления переменных определяют необходимость аннотаций
- Обобщённые типы — обобщённые типы возвращаемых значений часто требуют аннотаций
- Функции — параметры функций обычно требуют аннотаций
© 2013–present Facebook Inc.
Licensed under the MIT License.
https://flow.org/en/docs/lang/annotation-requirement