Spec-Zone.ru › Flow

Использование перечислений

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

Доступ к элементам перечисления​

Для доступа к элементам используйте точечный синтаксис. Вычисляемый доступ использовать нельзя:

enum Status {
  Active,
  Paused,
  Off,
}


// Access members with the dot syntax:
const status = Status.Active;


// You can’t use computed access:
const x = "Active";
Status[x]; // Error: computed access on enums is not allowed

Использование в качестве аннотации типа​

Объявление перечисления определяет и значение (через которое можно получить доступ к элементам и методам перечисления), и тип с тем же именем — тип элементов перечисления.

enum Status {Active, Paused, Off}


declare function calculateStatus(): Status;


const status: Status = calculateStatus();

Приведение к типу представления​

Перечисления не преобразуются неявно в свой тип представления и наоборот. Чтобы преобразовать значение типа перечисления в тип представления, можно использовать явное приведение x as string:

enum Status {Active, Paused, Off}


const s: string = Status.Active; // Error: 'Status' is not compatible with 'string'


declare const status: Status;
const statusString: string = status as string;

Можно также вызвать .valueOf() для значения перечисления. Это особенно полезно при работе с абстрактными перечислениями, когда тип представления неизвестен.

enum Status {Active, Paused, Off}


declare const status: Status;
const statusString: string = status.valueOf();

Чтобы преобразовать nullable-тип перечисления в nullable-тип представления, можно написать:

enum Status {Active, Paused, Off}
declare const maybeStatus: ?Status;


// Using `as` cast and `&&`:
const maybeStatusString1: ?string = maybeStatus && (maybeStatus as string);


// Using `.valueOf()` and optional chaining:
const maybeStatusString2: ?string = maybeStatus?.valueOf();

Чтобы преобразовать значение типа представления (например, string) в тип перечисления (если значение допустимо), см. описание метода cast.

Методы​

Объявления перечислений также определяют несколько полезных методов.

Ниже TEnum — это тип перечисления (например, Status), а TRepresentationType — тип представления этого перечисления (например, string).

.cast​

Тип: cast(input: ?TRepresentationType): TEnum | void

Метод cast позволяет безопасно преобразовать примитивное значение, например string, в тип перечисления (если это допустимое значение перечисления), а в противном случае возвращает undefined. Значение по умолчанию можно задать одной строкой с помощью оператора ??:

enum Status {Active, Paused, Off}
declare function getData(): string;
const data: string = getData();


const maybeStatus: Status | void = Status.cast(data);
if (maybeStatus != null) {
  const status: Status = maybeStatus;
  // do stuff with status
}


// Set a default value in one line with the `??` operator:
const statusWithDefault: Status = Status.cast(data) ?? Status.Off;

Тип аргумента cast зависит от типа перечисления. Для строкового перечисления типом аргумента будет string. Для числового перечисления типом аргумента будет number и так далее. Если нужно привести значение типа unknown, сначала используйте уточнение типа typeof:

enum Status {Active, Paused, Off}
declare const data: unknown;


if (typeof data === 'string') {
  const maybeStatus: Status | void = Status.cast(data);
}

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

enum Status {Active, Paused, Off}
declare const strings: Array<string>;


// WRONG: const statuses: Array<?Status> = strings.map(Status.cast);
const statuses: Array<?Status> = strings.map((input) => Status.cast(input)); // Correct

Затраты во время выполнения: для зеркальных строковых перечислений (например, enum E {A, B}), где имена элементов совпадают со значениями, затраты постоянны — эквивалентны вызову .hasOwnProperty. Для остальных перечислений при первом вызове создаётся Map, а последующие вызовы просто вызывают .has для кэшированной карты. Таким образом, амортизированные затраты постоянны.

.isValid​

Тип: isValid(input: ?TRepresentationType): boolean

Метод isValid похож на cast, но возвращает только логическое значение: true, если передано допустимое значение перечисления, и false в противном случае.

enum Status {Active, Paused, Off}
declare function getData(): string;


const data: string = getData();
const isStatus: boolean = Status.isValid(data);

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

enum Status {Active, Paused, Off}
declare const strings: Array<string>;


// WRONG: const statusStrings = strings.filter(Status.isValid);
const statusStrings = strings.filter((input) => Status.isValid(input)); // Correct

Затраты во время выполнения: такие же, как описано выше для .cast.

.members​

Тип: members(): Iterator<TEnum>

Метод members возвращает итератор (который является итерируемым объектом) по всем элементам перечисления.

enum Status {Active, Paused, Off}
declare function getButtonForStatus(status: Status): string;


const buttons = [];
for (const status of Status.members()) {
  buttons.push(getButtonForStatus(status));
}

Порядок итерации гарантированно совпадает с порядком элементов в объявлении.

Само перечисление не является перечисляемым или итерируемым объектом (например, цикл for-in/for-of по перечислению не будет перебирать его элементы); для этого нужно использовать метод .members().

Итерируемый объект можно преобразовать в Array с помощью Array.from(Status.members()). Можно использовать второй аргумент метода Array.from, чтобы одновременно с созданием массива преобразовать значения. Например:

enum Status {Active, Paused, Off}
declare function getButtonForStatus(status: Status): string;


const buttonArray = Array.from(
  Status.members(),
  status => getButtonForStatus(status),
);

.getName​

Тип: getName(value: TEnum): string

Метод getName сопоставляет значения перечисления со строковыми именами соответствующих элементов перечисления. При использовании перечислений number/boolean/symbol это может пригодиться для отладки и создания внутренних интерфейсов CRUD. Например:

enum Status {
  Active = 1,
  Paused = 2,
  Off = 3,
}
declare const status: Status;


console.log(Status.getName(status));
// Will print a string, either "Active", "Paused", or "Off" depending on the value.

Затраты во время выполнения: такие же, как описано выше для .cast. Для .cast, .isValid и .getName используется одна кэшированная обратная карта, сопоставляющая значения перечисления с именами. Первый вызов любого из этих методов создаёт эту кэшированную карту.

Исчерпывающая проверка перечислений с помощью switch​

При проверке значения перечисления в операторе switch мы требуем проверять все возможные элементы перечисления и не включать избыточные варианты. Отсутствующий вариант обозначается как [invalid-exhaustive-check], а в сообщении об ошибке указывается конкретный пропущенный элемент. Это помогает учитывать все возможности при написании кода, использующего перечисления. Особенно полезно это при рефакторинге: при добавлении или удалении элементов система укажет, где нужно внести изменения. Если включена поддержка match, вместо операторов switch используйте выражения и операторы match.

enum Status {Active, Paused, Off}
declare const status: Status;


// All members checked:
switch (status) {
  case Status.Active:
    break;
  case Status.Paused:
    break;
  case Status.Off:
    break;
}


// You can use `default` to match all members not checked so far:
switch (status) {
  case Status.Active:
    break;
  default: // When `Status.Paused` or `Status.Off`
    break;
}


// You can check multiple enum members in one switch case:
switch (status) {
  case Status.Active:
  case Status.Paused:
    break;
  case Status.Off:
    break;
}

Необходимо сопоставить все элементы перечисления (или добавить вариант default):

enum Status {
  Active = 1,
  Paused = 2,
  Off = 3,
}
const status: Status = Status.Active;


switch (status) { // Error: you haven't checked 'Status.Off' in the switch
  case Status.Active:
    break;
  case Status.Paused:
    break;
}

Повторять варианты нельзя (это приведёт к мёртвому коду!):

enum Status {
  Active = 1,
  Paused = 2,
  Off = 3,
}
const status: Status = Status.Active;


switch (status) {
  case Status.Active:
    break;
  case Status.Paused:
    break;
  case Status.Off:
    break;
  case Status.Paused: // Error: you already checked for 'Status.Paused'
    break;
}

Вариант default избыточен, если уже обработаны все случаи:

enum Status {
  Active = 1,
  Paused = 2,
  Off = 3,
}
const status: Status = Status.Active;


switch (status) {
  case Status.Active:
    break;
  case Status.Paused:
    break;
  case Status.Off:
    break;
  default: // Error: you've already checked all cases, the 'default' is redundant
    break;
}
// The following is OK because the `default` covers the `Status.Off` case:
switch (status) {
  case Status.Active:
    break;
  case Status.Paused:
    break;
  default:
    break;
}

Исключение — переключение по перечислению с неизвестными элементами.

Если внутри исчерпывающе проверяемых операторов switch есть вложенные исчерпывающе проверяемые операторы switch и из каждой ветви выполняется возврат, после вложенного оператора switch необходимо добавить break;:

enum Status {Active, Paused, Off}
declare const status: Status;
declare const otherStatus: Status;


function getCode(): number {
  switch (status) {
    case Status.Active:
      return 1;
    case Status.Paused:
      return 2;
    case Status.Off:
      switch (otherStatus) {
        case Status.Active:
          return 1;
        case Status.Paused:
          return 2;
        case Status.Off:
          return 3;
      }
      break;
  }
}

Не забывайте, что в варианты switch можно добавлять блоки. Они полезны, если нужно использовать локальные переменные:

enum Status {Active, Paused, Off}
declare const status: Status;
declare function f(): number;
declare function g(): number;


switch (status) {
  case Status.Active: {
    const x = f();
    break;
  }
  case Status.Paused: {
    const x = g();
    break;
  }
  case Status.Off: {
    const y = 3;
    break;
  }
}

Если бы в этом примере не было блоков, два объявления const x конфликтовали бы, что привело бы к ошибке.

Исчерпывающая проверка перечислений не выполняется в операторах if и других контекстах, кроме операторов switch и выражений/операторов match.

Исчерпывающая проверка перечислений с помощью match​

В конструкции match выполняется исчерпывающая проверка всех значений, включая перечисления Flow. Можно использовать шаблон-заполнитель _, чтобы сопоставить все ещё не проверенные элементы, а также проверять несколько элементов перечисления в одном варианте match с помощью шаблона «или»:

enum Status {Active, Paused, Off}
declare const status: Status;


// All members checked:
match (status) {
  Status.Active => {}
  Status.Paused => {}
  Status.Off => {}
}


// Using a wildcard `_` to match all members not checked so far:
match (status) {
  Status.Active => {}
  _ => {} // When `Status.Paused` or `Status.Off`
}


// Checking multiple enum members in one `match` case:
match (status) {
  Status.Active | Status.Paused => {}
  Status.Off => {}
}

Необходимо сопоставить все элементы перечисления (или добавить вариант _ с шаблоном-заполнителем):

enum Status {
  Active = 1,
  Paused = 2,
  Off = 3,
}
const status: Status = Status.Active;


match (status) { // Error: you haven't checked 'Status.Off' in the match
  Status.Active => {}
  Status.Paused => {}
}

Исчерпывающая проверка перечислений с неизвестными элементами​

Если в перечислении есть неизвестные элементы (указанные с помощью ...), например:

enum Status {
  Active,
  Paused,
  Off,
  ...
}

Тогда при переключении по перечислению всегда требуется default, а при использовании match всегда требуется вариант _ с шаблоном-заполнителем. Проверки default/_ обнаруживают «неизвестные» элементы, которые не были перечислены явно.

enum Status {
  Active,
  Paused,
  Off,
  ...
}
declare const status: Status;


switch (status) {
  case Status.Active:
    break;
  case Status.Paused:
    break;
  case Status.Off:
    break;
  default:
    // Checks for members not explicitly listed
}


match (status) {
  Status.Active => {}
  Status.Paused => {}
  Status.Off => {}
  _ => {
    // Checks for members not explicitly listed
  }
}

Можно использовать require-explicit-enum-switch-cases Flow Lint для switch или линтер require-explicit-enum-checks для match, чтобы требовать явного перечисления всех известных элементов в качестве вариантов. Например:

enum Status {
  Active = 1,
  Paused = 2,
  Off = 3,
  ...
}
const status: Status = Status.Active;


// flowlint-next-line require-explicit-enum-switch-cases:error
switch (status) { // Error
  case Status.Active:
    break;
  case Status.Paused:
    break;
  default:
    break;
}


// flowlint-next-line require-explicit-enum-checks:error
match (status) { // Error
  Status.Active => {}
  Status.Paused => {}
  _ => {}
}

Исправить это можно так:

enum Status {
  Active = 1,
  Paused = 2,
  Off = 3,
  ...
}
const status: Status = Status.Active;


// flowlint-next-line require-explicit-enum-switch-cases:error
switch (status) {
  case Status.Active:
    break;
  case Status.Paused:
    break;
  case Status.Off: // Added the missing `Status.Off` case
    break;
  default:
    break;
}


// flowlint-next-line require-explicit-enum-checks:error
match (status) {
  Status.Active => {}
  Status.Paused => {}
  Status.Off => {} // Added the missing `Status.Off` case
  _ => {}
}

Линтеры require-explicit-enum-switch-cases/require-explicit-enum-checks не включены глобально, а настраиваются отдельно для каждого switch/match, когда требуется такое поведение. Для обычных перечислений в каждом операторе switch или match можно добавить или не добавлять default/_, тем самым решив, нужно ли явно перечислять каждый вариант. Аналогично, для перечислений Flow с неизвестными элементами этот линтер можно включать отдельно для каждого switch/match.

Линтер require-explicit-enum-switch-cases также работает с операторами switch для обычных типов перечислений Flow. По сути, он запрещает использовать default в этом операторе switch, требуя явно перечислить все элементы перечисления в качестве вариантов.

Сопоставление перечислений с другими значениями​

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

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

Вместо:

enum Status {Active, Paused, Off}
declare const status: Status;


const STATUS_ICON: {[key: Status]: string} = {
  [Status.Active]: 'green-checkmark',
  [Status.Paused]: 'grey-pause',
  [Status.Off]: 'red-x',
};
const icon = STATUS_ICON[status];

Такой подход на самом деле не гарантирует, что каждому Status сопоставлено значение. Используйте:

enum Status {
  Active,
  Paused,
  Off,
}
declare const status: Status;


function getStatusIcon(status: Status): string {
  switch (status) {
    case Status.Active:
      return 'green-checkmark';
    case Status.Paused:
      return 'grey-pause';
    case Status.Off:
      return 'red-x';
  }
}
const icon = getStatusIcon(status);

Или с помощью match:

enum Status {
  Active,
  Paused,
  Off,
}
declare const status: Status;


const icon = match(status) {
  Status.Active => 'green-checkmark',
  Status.Paused => 'grey-pause',
  Status.Off => 'red-x',
};

Если в будущем вы добавите или удалите элемент перечисления, Flow сообщит, что нужно обновить также switch или match, чтобы сопоставление оставалось актуальным.

Если нужен словарь, для которого не требуется исчерпывающая проверка, можно использовать Map:

enum Status {Active, Paused, Off}
const counts = new Map<Status, number>([
  [Status.Active, 2],
  [Status.Off, 5],
]);
const activeCount: number | void = counts.get(Status.Active);

Элементы перечисления можно использовать как вычисляемые ключи в литералах объектов, если тип объекта задан с помощью типа с индексатором, как описано далее на этой странице.

Перечисления в объединённом типе​

Если значение перечисления входит в объединённый тип (например, ?Status), сначала уточните тип, чтобы оставить только тип перечисления:

enum Status {Active, Paused, Off}
declare const status: ?Status;


if (status != null) {
  status as Status; // 'status' is refined to 'Status' at this point
  switch (status) {
    case Status.Active: break;
    case Status.Paused: break;
    case Status.Off: break;
  }
}

Чтобы уточнить тип до значения перечисления, можно использовать typeof с типом представления. Например:

enum Status {Active, Paused, Off}
declare const val: Status | number;


// 'Status' is a string enum
if (typeof val === 'string') {
  val as Status; // 'val' is refined to 'Status' at this point
  switch (val) {
    case Status.Active: break;
    case Status.Paused: break;
    case Status.Off: break;
  }
}

Экспорт перечислений​

Перечисление — это и тип, и значение (как класс). Чтобы экспортировать и тип, и значение, экспортируйте его так же, как значение:

export enum Status {}

Или в качестве экспорта по умолчанию (примечание: имя перечисления указывать обязательно, export default enum {} не допускается):

export default enum Status {}

С использованием CommonJS:

enum Status {}
module.exports = Status;

Чтобы экспортировать только тип, но не значение, можно написать:

enum Status {}
export type {Status};

Другие функции в этом файле по-прежнему будут иметь доступ к реализации перечисления.

Импорт перечислений​

Если вы экспортировали перечисление следующим образом:

// status.js
export default enum Status {
  Active,
  Paused,
  Off,
}

Его можно импортировать и как значение, и как тип следующим образом:

import Status from 'status';

const x: Status /* used as type */ = Status.Active /* used as value */;

Если нужен только тип, его можно импортировать как тип:

import type Status from 'status';

function printStatus(status: Status) {
  ...
}

С использованием CommonJS:

const Status = require('status');

Абстрактные перечисления​

Можно писать универсальный код для работы с перечислениями Flow, используя два типа: Enum<> и EnumValue<>. Они принимают любое перечисление Flow и его значения.

EnumValue<>​

EnumValue<> принимает любое значение перечисления Flow. Нужно ограничить его значениями перечислений с определённым типом представления? Просто добавьте аргумент типа:

enum Status {Active, Paused, Off}


Status.Active as EnumValue<>; // OK
Status.Active as EnumValue<string>; // OK - its a string enum
Status.Active as EnumValue<number>; // ERROR - not a number enum

Новый тип EnumRepresentationTypes представляет все допустимые типы представления: string, number и так далее.

Значения перечислений Flow не преобразуются неявно в тип представления, поэтому для явного преобразования допускаются приведения, например Status.Active as string. При использовании EnumValue<> конкретный тип представления может быть неизвестен, поэтому теперь допускается приведение с помощью .valueOf() непосредственно для значения перечисления. Например:

function f(e: EnumValue<>) {
  const x: EnumRepresentationTypes = e.valueOf(); // OK
}

Enum<>​

Enum<> принимает любое перечисление Flow — само перечисление, а не его значение. При необходимости можно указать аргумент типа, чтобы ограничить типы перечислениями с определёнными значениями. Его можно использовать вместе с описанным выше типом EnumValue:

enum Status {Active, Paused, Off}


Status as Enum<>; // OK
Status as Enum<Status>; // OK
Status as Enum<EnumValue<string>>; // OK

С типом Enum<> можно использовать все методы перечислений Flow, например .cast и .members. Это позволяет писать универсальный код для работы с перечислениями Flow, например создавать полноценный компонент-селектор React, используя только перечисление Flow:

import * as React from 'react';
import {useCallback, useMemo, useState} from 'react';


component Selector<
  TEnumValue extends EnumValue<string>,
>(
  items: Enum<TEnumValue>,
  callback: (TEnumValue) => void,
) {
  // Typing the `useState` selector with the enum value generic
  const [value, setValue] = useState<?TEnumValue>(null);


  const handleChange = useCallback((e: SyntheticInputEvent<>) => {
    // Using the `.cast` method
    const value = items.cast(e.target.value);
    if (value == null) { throw new Error("Invalid value"); }
    setValue(value);
    callback(value);
  }, [items, callback]);


  return <ul>
    {Array.from(items.members(), item => // Using the `.members()` method
      <li>
        <label>
          {// Using the `.getName` method:
            items.getName(item)
          }:
          <input
            type="radio"
            // Casting to the representation type using `.valueOf()`
            value={item.valueOf()}
            checked={item === value}
            onChange={handleChange} />
         </label>
      </li>
    )}
  </ul>;
}


// Usage:
enum Status {
  Active,
  Paused,
  Off,
}
declare function doStuff(value: Status): void;


<Selector items={Status} callback={doStuff} />;

Когда не следует использовать перечисления​

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

Элементы перечисления как отдельные ключи объекта​

Элементы перечисления можно использовать как вычисляемые ключи свойств в литералах объектов. Полученный объект имеет тип с индексатором {[key: E]: T}:

enum Status {
  Active,
  Paused,
  Off,
}


const o = {
  [Status.Active]: 0,
};


o as {[key: Status]: number}; // OK
o[Status.Active] as number; // OK
o[Status.Paused] = 1; // OK

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

const LegacyStatus = Object.freeze({
  Active: 'Active',
  Paused: 'Paused',
  Off: 'Off',
});
const o = {
  [LegacyStatus.Active]: "hi",
  [LegacyStatus.Off]: 1,
};
const x: string = o[LegacyStatus.Active]; // OK
const y: number = o[LegacyStatus.Off]; // OK
const z: boolean = o[LegacyStatus.Active]; // Error - as expected

Это работает, потому что LegacyStatus.Active имеет литеральный тип 'Active', а LegacyStatus.Off — литеральный тип 'Off'. В перечислениях Flow все элементы имеют общий тип перечисления, поэтому Flow не может различать ключи — для всех ключей перечисления у объекта может быть только один тип значения (через индексатор {[key: E]: T}).

Если нужно сопоставить каждому элементу перечисления значение своего типа, используйте функцию с исчерпывающе проверяемым оператором switch.

Непересекающиеся объединения объектов​

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

type Action =
  | {type: 'Upload', data: string}
  | {type: 'Delete', id: number};

У каждого типа объекта в объединении есть одно общее поле (type), которое используется для определения типа рассматриваемого объекта.

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

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

Гарантированное встраивание​

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

Доступ к элементам перечисления (например, Status.Active) можно встроить (за исключением символьных перечислений, которые нельзя встроить из-за особенностей символов), но вызовы его методов (например, Status.cast(x)) встроить нельзя.

См. также​

  • Объединённые типы — альтернатива перечислениям для некоторых сценариев использования
  • Сопоставление — сопоставление с образцом хорошо сочетается с перечислениями и позволяет выполнять исчерпывающую проверку

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

Spec-Zone.ru

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