Сужение типов
Сужения позволяют нам сузить тип значения на основе условных проверок.
Например, в приведённой ниже функции value — это объединение "A" или "B".
function func(value: "A" | "B") {
if (value === "A") {
value as "A";
}
} Внутри блока if мы знаем, что значение должно быть "A", потому что только в этом случае условие if будет истинным.
Способность статического анализатора типов определить, что значение внутри оператора if должно иметь тип "A", называется сужением типа.
Теперь добавим блок else в наш оператор if.
function func(value: "A" | "B") {
if (value === "A") {
value as "A";
} else {
value as "B";
}
} Внутри блока else мы знаем, что значение должно быть "B", потому что оно может быть только "A" или "B", а мы исключили "A" из возможных вариантов.
И Flow, и TypeScript сужают типы с помощью typeof / instanceof / проверок на равенство / проверок типов, но правила, определяющие, когда сужение перестаёт действовать, различаются. Подробнее см. в разделе Сравнение отмены сужений в Flow и TypeScript.
Способы сужения типов в Flow
Проверки typeof
С помощью проверки typeof value === "<type>" можно сузить тип значения до одной из категорий, поддерживаемых оператором typeof.
Оператор typeof может возвращать "undefined", "boolean", "number", "bigint", "string", "symbol", "function" или "object".
Имейте в виду, что оператор typeof возвращает "object" для объектов, а также для null и массивов.
function func(value: unknown) {
if (typeof value === "string") {
value as string;
} else if (typeof value === "boolean") {
value as boolean;
} else if (typeof value === "object") {
// `value` could be null, an array, or an object
value as null | interface {} | ReadonlyArray<unknown>;
}
} Чтобы проверить значение на null, используйте проверку на равенство с value === null.
function func(value: unknown) {
if (value === null) {
value as null; // `value` is null
}
} Для проверки на массивы используйте Array.isArray:
function func(value: unknown) {
if (Array.isArray(value)) {
value as ReadonlyArray<unknown>; // `value` is an array
}
} Проверки на равенство
Как показано во вступительном примере, с помощью проверки на равенство можно сузить тип значения до конкретного. Это также относится к проверкам на равенство в операторах switch.
function func(value: "A" | "B" | "C") {
if (value === "A") {
value as "A";
} else {
value as "B" | "C";
}
switch (value) {
case "A":
value as "A";
break;
case "B":
value as "B";
break;
case "C":
value as "C";
break;
}
} Хотя в целом не рекомендуется использовать == в JavaScript из-за выполняемых им преобразований типов, проверки value == null (или value != null) точно соответствуют проверкам value для null и void. Это хорошо работает с типами maybe в Flow, которые создают объединение с null и void.
function func(value: ?string) {
if (value != null) {
value as string;
} else {
value as null | void;
}
} Можно сузить объединение типов объектов на основе общего тега; такие объединения называются непересекающимися объединениями объектов:
type A = {type: "A", s: string};
type B = {type: "B", n: number};
function func(value: A | B) {
if (value.type === "A") {
// `value` is A
value.s as string; // Works
} else {
// `value` is B
value.n as number; // Works
}
} Проверки истинности
В условиях JavaScript можно использовать значения, не являющиеся логическими. 0, NaN, "", null и undefined преобразуются в false (и поэтому считаются «ложными»). Остальные значения преобразуются в true (и поэтому считаются «истинными»).
function func(value: ?string) {
if (value) {
value as string; // Works
} else {
value as null | void; // Error! Could still be the empty string ""
}
} Из примера выше видно, почему не рекомендуется проверять истинность значения, если оно может быть строкой или числом: так можно случайно проверить, не равно ли оно "" или 0. Мы создали правило линтера Flow под названием sketchy-null, чтобы защититься от такой ситуации:
// flowlint sketchy-null:error
function func(value: ?string) {
if (value) { // Error!
}
}
Проверки in
С помощью оператора in можно проверить, существует ли свойство у объекта (среди его собственных свойств или в цепочке прототипов). Это можно использовать для сужения объединения объектов:
function func(obj: {foo: string, value: boolean} | {bar: string, value: number}) {
if ('foo' in obj) {
obj.value as boolean; // Works!
} else {
obj.value as number; // Works!
}
} Лучше всего это работает с объединениями точных объектов, поскольку при отрицательном результате мы знаем, что свойства нет. То же нельзя утверждать для неточных объектов, интерфейсов и типов экземпляров, поскольку у них могут быть другие неизвестные свойства, включая то, которое мы проверяем. Кроме того, необязательные свойства могут существовать, а могут и отсутствовать, поэтому проверять их не особенно полезно.
Чтобы сузить объединение типов кортежей в зависимости от наличия элемента, проверяйте свойство length, а не пытайтесь использовать in.
Проверки instanceof
Сузить тип значения можно также с помощью оператора instanceof. Он проверяет, находится ли прототип переданного конструктора где-либо в цепочке прототипов значения.
class A {
amaze(): void {}
}
class B extends A {
build(): void {}
}
function func(value: unknown) {
if (value instanceof B) {
value.amaze(); // Works
value.build(); // Works
}
if (value instanceof A) {
value.amaze(); // Works
value.build(); // Error
}
if (value instanceof Object) {
value.toString(); // Works
}
} Присваивания
Flow отслеживает поток управления и сужает тип переменной после присваивания ей значения.
declare const b: boolean; let x: ?string = b ? "str" : null; x as ?string; x = "hi"; // We know `x` must now be a string after the assignment x as string; // Works
Проверки типов
Можно создать повторно используемое сужение, определив функцию, которая является проверкой типа.
function nonMaybe<T>(x: ?T): implies x is T {
return x != null;
}
function func(value: ?string) {
if (nonMaybe(value)) {
value as string; // Works!
}
} Ограничения
Отмена сужений
Flow отменяет сужение, если промежуточный код мог изменить значение в месте хранения, тип которого был сужен. В частности, сужение отменяется, когда:
- В суженную переменную или свойство выполняется запись (
x = ...,obj.k = ...). - В области видимости находится сужение свойства объекта, и к свойству можно получить доступ через псевдоним либо вызываемая функция может его изменить (простой вызов между проверкой и использованием отменяет сужение).
- В области видимости находится сужение переменной, захваченной замыканием, и промежуточный вызов может вызвать это замыкание.
Сам по себе простой вызов функции, которая явно не затрагивает место хранения суженного значения, не отменяет сужение локальной переменной. Чаще всего здесь возникает излишняя перестраховка: разработчики считают, что любой вызов функции отменяет сужения, хотя на деле только сужения свойств и переменных, захваченных замыканиями, считаются ненадёжными после вызовов.
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
}
} Сужение свойства отменяется после вызова, потому что мы не знаем, не изменила ли вызываемая функция свойство через псевдоним. Рассмотрим следующий сценарий:
const obj: {prop?: string} = {prop: "test"};
function otherFunc() {
if (Math.random() > 0.5) {
delete obj.prop;
}
}
function func(value: {prop?: string}) {
if (value.prop) {
otherFunc();
value.prop.charAt(0); // Error!
}
}
func(obj); Внутри otherFunc() мы иногда удаляем prop. Flow не знает, остаётся ли проверка if (value.prop) истинной, поэтому отменяет сужение.
Объявление типа параметра как доступного только для чтения (Readonly<{prop?: string}> или {readonly prop?: string}) не помогает: readonly ограничивает только действия, которые этот код может выполнять через это представление, а у вызываемой функции может быть ссылка на тот же объект, не ограниченная только чтением.
Эту проблему легко обойти: сохраните значение перед вызовом другой функции и используйте сохранённое значение. Так можно избежать отмены сужения.
function otherFunc() { /* ... */ }
function func(value: {prop?: string}) {
if (value.prop) {
const prop = value.prop;
otherFunc();
prop.charAt(0);
}
} Нельзя сужать тип деструктурированных переменных
При деструктуризации значения из дискриминированного объединения каждая полученная переменная сохраняет полный тип объединения независимо от остальных. Сужение типа деструктурированной переменной-маркера не сужает типы соседних переменных:
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
} Решение — сужать тип исходного значения, а не деструктурировать его заранее:
type FormField =
| {kind: 'text', value: string}
| {kind: 'number', value: number};
declare const field: FormField;
if (field.kind === 'text') {
const s: string = field.value; // OK
} Другой вариант — использовать выражение или оператор match, который выполняет сопоставление и деструктуризацию одновременно: в каждой ветви проверяется маркер, а value получает тип, соответствующий этой ветви:
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;
}
} Сужение обобщённого значения не сужает типы других значений с тем же параметром типа
Если функция обобщённая, сужение одного параметра на основе проверки во время выполнения не сужает другие параметры с тем же параметром типа. Flow рассматривает параметр типа как единый фиксированный тип для всего вызова: сужение значения одного параметра не меняет сам тип T.
type Row = {kind: 'row', cols: number};
type Grid = {kind: 'grid', rows: number, cols: number};
type Layout = Row | Grid;
function updateLayout<T extends Layout>(
layout: T,
setLayout: (value: T) => void,
): void {
if (layout.kind === 'row') {
// `layout` is narrowed to `Row`, but `setLayout` is still `(T) => void`
setLayout({kind: 'row', cols: 3}); // ERROR
}
} Чтобы обойти это ограничение, откажитесь от обобщённого типа и используйте вместо него конкретный тип объединения. Тогда сужение работает как ожидается, потому что setLayout принимает любое значение типа Layout:
type Row = {kind: 'row', cols: number};
type Grid = {kind: 'grid', rows: number, cols: number};
type Layout = Row | Grid;
function updateLayout(
layout: Layout,
setLayout: (value: Layout) => void,
): void {
if (layout.kind === 'row') {
setLayout({kind: 'row', cols: 3});
}
} См. также
- Проверки типов — пользовательские функции, выполняющие роль пользовательских сужений
- Объединения — чаще всего сужения используются для сужения типов объединений
-
Типы maybe — сужение допускающих значение null типов с помощью проверок
!= null -
Сужение типа
unknown— приёмы, предназначенные для сужения значений типаunknown - Переменные — взаимодействие объявлений переменных с сужением типов
- Выражения match — сопоставление с образцом с собственным сужением типов и проверкой полноты вариантов
© 2013–present Facebook Inc.
Licensed under the MIT License.
https://flow.org/en/docs/lang/refinements