Flowのenumについて

Flowの更新をおっかけていたら、enumが導入されていて、TypeScriptのenumと結構違っていて面白かったので紹介します。

# Flowのenumの始め方

- Flow 0.159以上
- [flow-enums-runtime](https://www.npmjs.com/package/flow-enums-runtime)が必要

詳しくは https://flow.org/en/docs/enums/enabling-enums/ ここを参照してください。runtimeを必要とするというのが面白いです。

すぐ遊べるようにGitHubに設定済みのプロジェクトを用意しておきました。

https://github.com/mk2/flow-enum-demo

# Flowのenumで遊ぶ

## 作成

下記のようにenumを作成します。

```javascript
enum Status {
  Active,
  NonActive,
}
```

- 小文字始まりはNG（Enumのメソッド名として確保済みらしい）

という規則があり、ほぅ…？という気分にさせてくれます。

## 色んなenum

### 文字列のenum

何も気にせずにenumを作ると、文字列で実際の値が表現されるenumが生成されます。TypeScriptは数値型だったと思うので、もうここから全然違いますね。

```javascript
enum Status {
  Active,
  NonActive,
}
assert.equal(Status.Active, "Active");
```

もちろん文字列の値を任意のものにする事も出来ます。

```javascript
enum StatusExplicitString {
  Active = "Active",
  NonActive = "NonActive",
}
assert.equal(StatusExplicitString.Active, "Active");
assert.equal(Status.Active, StatusExplicitString.Active);
```

さらに文字列型のenumであることを明示的に宣言する事も出来ます。

```javascript
enum StatusExplicitOfString of string {
  Active,
  NonActive,
}
assert.equal(StatusExplicitOfString.Active, "Active");
assert.equal(Status.Active, StatusExplicitOfString.Active);
```

### 数値のenum

なんと明示的に全ての値を入れる必要があります。最初だけ1で後は増やしてってーというのは出来ないわけですね。

>We do not allow defaulting of number enums (unlike some other languages), because if a member from the middle of such an enum is added or removed, all subsequent member values would be changed. This can be unsafe (e.g. push safety, serialization, logging). Requiring the user to be explicit about the renumbering makes them think about the consequences of doing so.

>DeepL翻訳: このような列挙型の途中からメンバーを追加・削除すると、それ以降のメンバー値がすべて変更されてしまうため、（他の言語と異なり）数列型の列挙型のデフォルトを許可していません。これは安全でない可能性がある（例：プッシュセーフティ、シリアライゼーション、ロギングなど）。番号の付け替えについてユーザに明示的に要求することで、そうすることの結果について考えさせることができる。

文字列型と同じように`enum ... of number`で数値型であることを明示的に宣言できます。

```javascript
enum Rank {
  First = 1,
  Second = 2,
  Third = 3,
}

enum RankExplicitOfNumber of number {
  First = 1,
  Second = 2,
  Third = 3,
}
assert.equal(Rank.First, RankExplicitOfNumber.First);
```

### 真偽値のenum

真偽値のenumも作る事が出来ます。ちなみにtrue/falseのみなので2つの値限定です。

```javascript
enum YesNo {
  Yes = true,
  No = false,
}

enum YesNoExlicitOfBoolean of boolean {
  Yes = true,
  No = false,
}
assert.equal(YesNo.Yes, YesNoExlicitOfBoolean.Yes);
```

### シンボルのenum

シンボル値をもつenumも作る事が出来ます。シンボル型はその宣言の問題上、`enum ... of symbol`で明示的にシンボル値であることを宣言してやらねばいけません。（`of symbol`がないとデフォルトの文字列値のenumと区別出来ない）

```javascript
enum OS of symbol {
  Mac,
  Windows,
  Linux,
}
assert.equal(OS.getName(OS.Mac), "Mac");
```

`OS.getName(...)`ってなんだ！？と思うかも知れませんが、Flowのenumはランタイム時にも存在するオブジェクトなので、メソッドを使う事が出来るのです :metal:

### 未知のメンバー

**enumなのに未知のメンバーだと…？**と思うかもしれませんが、流石はFlowのenum、そういう機能を持っています。↓のように`...`をメンバーに含めます。`...`をメンバーに入れると、switch文でenumをチェックしたとき、enumの値をすべてcaseでチェックしていたとしても、default節を要求されます。

```javascript
enum GoPro {
  GoPro9,
  GoPro10,
  ...
}
const myGoPro = GoPro.GoPro9;

switch (myGoPro) {
  case GoPro.GoPro9:
    break;
  case GoPro.GoPro10:
    break;
  default:
    // このデフォルト節を省略できない
    break;
}
```

これの使いどころとしては、下記のような例が挙げられてます。

>This feature is useful when an enum value crosses some boundary and the enum declaration on each side may have different memebers. For example, an enum definition which is used on both the client and the server: an enum member could be added, which would be immediately seen by the server, but could be sent to an outdated client which isn’t yet aware of the new member.One use case for this would be the JS output of GraphQL Enums: Flow Enums with unknown members could be used instead of the added '%future added value' member.

> DeepL翻訳: この機能は、enumの値が何らかの境界をまたぎ、それぞれの側のenum宣言が異なるメンバーを持つ可能性がある場合に有効である。例えば、クライアントとサーバーの両方で使用される enum 定義では、enum のメンバーが追加され、サーバーからはすぐに見えますが、新しいメンバーをまだ認識していない古いクライアントには送られる可能性があります。この使用例としては、GraphQL Enums の JS 出力があります。未知のメンバーを持つFlow Enumsは、追加された'%future added value'メンバーの代わりに使用されるかもしれません。

なるほど…？（GraphQLよく分からんのでワカッテルフリ）。使う前はよく分からんけど、実際使う場面に遭遇すると「あぁ〜〜！」となりそうな機能な気がします。

## ネーミングガイドがある

ちょっとenum遊びからは外れるのですが、Flowのドキュメントはenumの命名についてガイドを用意してくれてます。

### enumの名付け（`Naming enums`)

- 全て大文字はやめろ
- enumの型名は単数系にしろ
- `Enum`という文字列を型名に入れるな

> We encourage you to define enum names in PascalCase, following the naming conventions of other types. All caps names (e.g. STATUS) are harder to read and discouraged.We encourage you to name enums in the singular. E.g. Status, not Statuses. Just like the type of true and false is boolean, not booleans.Don’t append Enum to the name (e.g. don’t name your enum StatusEnum). This is unnecessary, just like we don’t append Class to every class name, and Type to every type alias.

> DeepL翻訳: 他の型の命名規則に従って、PascalCaseでenum名を定義することを推奨します。全て大文字の名前（例：STATUS）は読みにくく、推奨されません。また、列挙型の名前は単数形にすることを推奨します。例：Status、Statuses ではない。ちょうど、true と false の型が boolean であるように、boolean ではない。Enum を名前に追加しないでください (例: Enum に StatusEnum という名前をつけないでください)。これは、すべてのクラス名にClassを、すべての型の別名にTypeを付けないのと同じで、不要なことです。

### enumのメンバーの名付け（`Naming enum members`）

- メンバーはPascalCaseで定義しろ
- 全部大文字にするな

> We encourage you to define enum member names in PascalCase. All caps names (e.g. ACTIVE) are harder to read and discouraged. Additionally, since Flow enforces that these are constants, you don’t need to use the name to signal that intent to the programmer.

> DeepL翻訳: enumのメンバー名は、PascalCaseで定義することをお勧めします。全角の名前（例：ACTIVE）は読みにくく、推奨されません。さらに、Flowはこれらが定数であることを強制するので、プログラマにその意図を伝えるために名前を使用する必要はない。

なんかこう…enumの命名で色々苦労してきたんだろうな、と感じますね。Flowでenumを使うときはガイドに従いましょう。

## enumの使い方

### 型変換

文字列値のenumであっても、string型の変数にさくっと入れる事は許されません。明示的なキャストが必要です。Flowのキャストは括弧（`()`）で括ります。

```javascript
// 次の行はエラーになる
// const s: string = Status.Active;
// 次の行はえらーにならない
const s: string = (Status.Active: string);
```

### enumのメソッドたち

先ほどのenumの作り方でさらっとお見せしましたが、Flowのenumは実行時にオブジェクトでありプロトタイプを持つので、色々メソッドが生やされています。

#### `.cast`

文字列から文字列値のenumを作りたい、などの場面で使います。castできないものを渡されるとundefinedを返すようです。

```javascript
const castedStatus: Status | void = Status.cast("Active");
assert.equal(castedStatus, Status.Active);
const failedStatus: Status | void = Status.cast("hoge");
assert.equal(failedStatus, undefined);
```

#### `.isValid`

castのboolean版ですね。実際に変換するのではなくcast出来るかどうかを教えてくれます。

```javascript
assert.equal(Status.isValid("Active"), true);
assert.equal(Status.isValid("hoge"), false);
```

#### `.members`

enumのメンバーのイテレーターを返してくれます。

```javascript
assert.deepEqual([...Status.members()], ["Active", "NonActive"]);
assert.deepEqual([...Rank.members()], [1, 2, 3]);
```

#### `.getName`

メンバーを渡すとその名前を返してくれます。なんかJavaのenumも同じようなメソッドがあった記憶。

```javascript
assert.equal(Status.getName(Status.Active), "Active");
assert.equal(Rank.getName(Rank.First), "First");
```

## TypeScriptのenumとの比較

https://medium.com/flow-type/typescript-enums-vs-flow-enums-83da2ca4a9b4

FlowチームがTypeScriptのenumと比較した記事を書いてくれています。ただ、そもそもTypeScriptにおいてenumはあまり推奨されていないイメージなので（特に数値型のenum）、色々反省点を踏まえて作られたであろうFlowのenumが良さそうなのは、まぁそうだろうなという気がしてます。

### switch文での網羅的なチェック

Flowはswitch文で網羅的にチェックする事を要求されるが、TypeScriptはチェックを忘れていても教えない。（でも普通ESLintでチェックしないか…？）

### 暗黙的な型変換

Flowのenumは暗黙的なenum型への変換を許可しないが、TypeScriptのenumでは暗黙的なenumへの型変換が許可されてしまう（特に数値型のenum）。

これはTypeScripterは皆知ってますね。これがあるので数値型のenumはTypeScriptでは使ってはいけないというのが規範となっていると思います。Flowはここをちゃんと改善してきているわけですね。

### デフォルト数値

Flowの数値enumではデフォルトの値というのは存在せず、すべて明示的に設定する必要がある。

TypeScriptが勝手に値を設定するのと対照的ですね。実際メンバーを入れ替えるだけで簡単に値が変わってしまうので、個人的にはこの方針は非常に良いと思います。

### enumの継承

Flowではenumの継承や修正は出来ず、freezeされたオブジェクトのように振る舞う。TypeSciprtではenumは継承や修正されてしまうので（これ知らなかった）、非常に扱い辛い。

記事にある例を見るとその怖さが分かります。

```typescript
// TypeScript:
enum Status {
  Active = 0,
  Paused = 1,
  Off = 2
}
enum Status {
  // Due to TypeScript's default values, this is implicitly `0`:
  Disabled,
}
console.log(Status.Disabled === Status.Active); //=> true
```

### リバースマッピング

Flowは`getName`メソッドでメンバーの名前をとれるが、TypeScriptの場合は数値型のみ同じような機能をサポートしていて、文字列型のenumでは`getName`と同等の事は出来ない。

### enumメンバーを反復処理する

Flowは`members`メソッドで全てのメンバーを反復処理出来るイテレーターを取得できるが、TypeScriptにその方法はない。

実際ないので、めちゃくちゃ困った記憶があります。

### シンボルenum

Flowはシンボル値を持ったenumを作成できる。

これは結構有用ですよね。欲しい場面は多そう。

### 制限された定義

Flowのenumはより制限されたものである。

> Flow Enums are more restrictive than TypeScript enums when it comes to their definition. The general philosophy of the Flow Enums design was to start off being restrictive, and wait to see actual use-cases and requests before expanding functionality. It is always easier to loosen up constraints than tighten them up.Unlike TypeScript, Flow Enum members must:• All have the same type (no heterogeneous enums)• Be literals, if a value is specified (no arbitrary expressions)• Either have all of their values specified, or all defaulted• Have unique member values• Have their member names start with an uppercase letter (lowercase starting letters are reserved for the enum methods like .cast)You can read more about defining Flow Enums.

> DeepL翻訳: Flow Enumsは、TypeScriptのenumよりも定義に制約がある。Flow Enumsの設計の一般的な哲学は、最初は制限的であり、機能を拡張する前に実際のユースケースと要求を見るのを待つことであった。TypeScriptとは異なり、Flow Enumのメンバは以下の条件を満たす必要がある： - すべて同じ型を持つ（異種列挙は不可） - 値が指定されている場合はリテラルである（任意の表現は不可） - すべての値を指定するか、すべてをデフォルトとする - ユニークなメンバ値を持つ - メンバ名が大文字で始まる（小文字で始まる文字は.castなどの列挙メソッドのために予約されている）Flow Enumsの定義についてもっと読むには、「Flow Enum」を参照してください。

### インライン化

Flowのenumはランタイムを必要としますが、実際にはインライン化出来るようです。ほーん。

> TypeScript provides “const enums”, which allow you to specify at the definition site that this enum will be inlined when using TypeScript’s supplied build system.Flow Enums are designed to have properties that make inlining possible (e.g. member values must be literals, and the enum definition is frozen) other than for enum methods usage and symbol enums, which cannot be inlined by their nature. Rather than implement inlining in Flow, we leave the implementation of that to whatever build system you are using. We view enum inlining as an optimization performed by the build system rather than a feature of the language. Internally at Facebook we have implemented inlining for many cases of Flow Enums in our primary internal build system.

> DeepL翻訳: TypeScriptには「const enum」というものがあり、TypeScriptが提供するビルドシステムを使用する際に、このenumをインライン化することを定義サイトで指定できる。Flow Enumsは、その性質上インライン化できないenum methods usageとsymbol enumを除き、インライン化できるプロパティ（例：メンバー値はリテラルを指定、enum定義の凍結）を持って設計されている。Flowでインライン化を実装するのではなく、その実装は使用するビルドシステムに委ねます。enumのインライン化は、言語の機能ではなく、ビルドシステムが行う最適化であると捉えています。Facebook内部では、我々の主要な内部ビルドシステムにおいて、Flow Enumの多くのケースでインライン化を実装しています。