TypeScriptのenum
について。
あちこちで「enum
はアカンからunion型を使え」と言われていますが、その理由や「本当にenum
はいいところないの?」「unionはenum
の移行先として適切なの?」などをいろいろ検証してみましょう。
最後に「こういう仕様ならいいのに」という妄想もあります。
enum
についておさらい
いわゆる列挙型です。CやC++であるようなアレです。定数リテラルをシンボル化して、IDEの補完やら何やらに使えるアレです。
いいところ👍
- 読みやすい
- リファクタリングしやすい
const enum
ならゼロオーバーヘッド-
要素が文字列だけなら
Object.values(StringEnum)
で要素の配列を取得できる - シンボルのエイリアスを定義できる
const enum
は型チェックのみ行い、JSへトランスパイルした後は綺麗サッパリ消えてくれます。そのため生成されたJSを実行するときもオーバーヘッドが一切発生しません。
最後の「シンボルのエイリアス」というのは何かというと、例えば
// 数値文字列のチェックサムアルゴリズム enum CHECKSUM_ALGORITHM { LUHN = "luhn", // Luhnアルゴリズム CREDIT_CARD = LUHN, // クレジットカード(Luhnアルゴリズムと同じ) MODULUS10_WEIGHT3_1 = "modulus10/weight3:1", // modulus10/weight3:1 ISBN13 = MODULUS10_WEIGHT3_1, // ISBN-13(modulus10/weight3:1と同じ) EAN = MODULUS10_WEIGHT3_1, // EAN code(modulus10/weight3:1と同じ) JAN = MODULUS10_WEIGHT3_1, // JAN code(modulus10/weight3:1と同じ) }
のような書き方ができます。
わるいところ👎
-
const enum
にしないとわけわからんオブジェクトができる const enum
はアンビエント宣言できない-
const enum
は要素をObject.values(StringEnum)
で列挙できない - 数値型には値のチェック機構が働かない
まず作られるオブジェクトがカオスすぎる。なんでわざわざ関数かましてんだ。せめてオブジェクトにとどめてくれ。
アンビエント宣言は、*.d.ts
のアレです。npmモジュールとして外部に晒すときに使うアレです。
const enum
はわけわからんオブジェクトを作らずに済むのですが、*.d.ts
に書けません。いえ、正確には書けるのですが、それを外部からアクセスしようとするとエラーになります。結果としてnpmモジュールとしてexportする必要のある型にはconst enum
を使えません。
最後の項目はどういうことかというと、例えば以下のようなコードがエラーにならずに通ってしまいます。
enum NUMBER { ZERO = 0, ONE = 1, TWO = 2, } const num: NUMBER = 3; // NUMBERにない値を設定できてしまう
これはどうやら意図した挙動のようで、以下のようなビット演算に使うような場合を想定しているそうです。
enum MASK { MASK1 = 0x01, MASK2 = 0x02, MASK3 = 0x04, MASK4 = 0x08, } const mask: MASK = MASK.MASK1 | MASK.MASK2; // 値は0x03
unionについておさらい
number | string
のように「数値型または文字列型」のような型を作れるアレです。リテラル型のunionを使えばenum
と実質的に同等のことができるぞ!というわけです。
type CHECKSUM_ALGORITHM = "luhn" | "modulus10/weight3:1"; const checksum1: CHECKSUM_ALGORITHM = "luhn"; const checksum2: CHECKSUM_ALGORITHM = "abc"; // エラー("luhn"または"modulus10/weight3:1"しか代入できない)
enum
をunionで置き換える方法
上で挙げたサンプルコードだと、確かに値は制限されるけどシンボルで置き換えているわけじゃないし使いにくいですね。
そこで、オブジェクトとkeyof typeof
を使えばenum
と同じように使える型が出来上がります。
const CHECKSUM_ALGORITHM = { LUHN: "luhn", CREDIT_CARD: "luhn", MODULUS10_WEIGHT3_1: "modulus10/weight3:1", ISBN13: "modulus10/weight3:1", EAN: "modulus10/weight3:1", JAN: "modulus10/weight3:1", } as const; type CHECKSUM_ALGORITHM = typeof CHECKSUM_ALGORITHM[keyof typeof CHECKSUM_ALGORITHM]; // シンボルで書ける const checksum: CHECKSUM_ALGORITHM = CHECKSUM_ALGORITHM.CREDIT_CARD;
うん、まあ書けるっちゃ書けるけど一手間多いですね。元々enum
とunionは目的が違うので、どうしても列挙用途ではenum
のほうがシンプルに書けるのは仕方ない。イディオムみたいなものだと思えばいいんでしょうか。
また、実際にオブジェクトが作られる分const enum
よりもわずかにオーバーヘッドがあります。enum
ほどじゃないですが。
ただし、実際にオブジェクトが作られるのでObject.values()
で要素を列挙できるという利点はあります。
enum
をunionで置き換えられないケース
定義の方法がちょっとややこしいというのは置いておくとしても、それ以外にenum
のほうが便利だったり単純にenum
をunionで置き換えられないケースというものも存在します。
まずは、unionではシンボルのエイリアスを定義できないという点。上のCHECKSUM_ALGORITHM
の例を見てみるとわかりますが、LUHN
とCREDIT_CARD
に同じ文字列"luhn"
を割り当てているのでDRYの原則から外れてしまいます。enum
ではCREDIT_CARD
にLUHN
を代入するだけで済みました。
union版は定義したシンボルはあくまでオブジェクトに過ぎません。シンボルのエイリアスが必要になる場面はあまりないかもしれませんが、いざ必要になったときに慌てないように知識として覚えておいてもいいと思います。
もう1つは、数値型の値のチェックができてしまうという点。enum
版では通った以下のようなコードがエラーになってしまいます。
const MASK = { MASK1: 0x01, MASK2: 0x02, MASK3: 0x04, MASK4: 0x08, } as const; type MASK = typeof MASK[keyof typeof MASK]; const mask: MASK = MASK.MASK1 | MASK.MASK2; // エラー; 0x03をMASK型に代入できない
これも必要になる場面はあまりないと思いますが。
unionはenum
の移行先として適切か?
まず適切かどうかではなく使えるかどうかで判断するなら、だいたいの場合は使えます。ただし、元々の作られた目的が違うため、どうしてもunionだと冗長な書き方になってしまいます。
unionへ置き換える前に、まずはconst enum
を使えないかを検討しましょう。それでも解決しない場合にunionを使いましょう。
TypeScriptチームへ期待すること
注:このセクションの内容は完全な妄想です。ここに書いてあるような文法やキーワードはTypeScriptにはありませんし、実装予定でもありません。要望も出していません。「こう使えたらいいのにな」という妄想です。
もしかしたらすでにissueが出ているかもしれませんが、unionのトリックを使わなくても済むようにenum
を強化してもらえると素敵です。
具体的には
- デフォルトで
const enum
相当の動作(ゼロオーバーヘッド) - アンビエント宣言を外部から使える
あたりでしょうか。現在アンビエント宣言されたconst enum
を外部から使えない仕様なのは何ででしたっけ。言語仕様的に何か問題あるのかな。
const enum
相当だと要素を列挙できない問題については、enum
の中身を反復可能なオブジェクトとして返す新しいキーワードを導入するとかどうでしょう。
enum FOO { Foo = "foo", Bar = "bar", Baz = "baz", } // enumerateキーワードで中身を列挙 for (const foo of enumerate FOO) { console.log(foo); }
// こんな感じにトランスパイルされる(FOOの中身がその場で展開される) for (const foo of ["foo", "bar", "baz"]) { console.log(foo); }
数値型のアレはなぁ・・・デフォルトの挙動は今のままで仕方ないとして、列挙された値しか許可したくない場合は・・・例えばstrict enum
みたいな文法はどうですかね。
// 範囲外の数値を使えないようにするstrict enum strict enum MASK { MASK1 = 0x01, MASK2 = 0x02, MASK3 = 0x04, MASK4 = 0x08, } const mask: MASK = MASK.MASK1 | MASK.MASK2; // エラー; 0x03をMASK型に代入できない
やっぱり列挙型の問題は無理にunionを使わずにenum
を強化するのが一番いいと思うんだな。
0 件のコメント:
コメントを投稿