列挙型(enum)の問題点と代替手段
TypeScriptの列挙型(enum)にはいくつか問題点が指摘されていてます。ここでは、その問題点と代替手段を説明します。
列挙型の問題点
列挙型はTypeScript独自すぎる
TypeScriptは、JavaScriptを拡張した言語です。拡張といっても、むやみに機能を足すのではなく、追加するのは型の世界に限ってです。こういった思想がTypeScriptにはあるため、型に関する部分を除けば、JavaScriptの文法から離れすぎない言語になっています。
JavaScriptの文法からドラスティックに離れたAltJSもあります。その中で、TypeScriptが多くの開発者に支持されているのは、JavaScriptから離れすぎないところに魅力があるからというのもひとつの要因です。
TypeScriptの列挙型に目を向けると、構文もJavaScriptに無いものであるだけでなく、コンパイル後の列挙型はJavaScriptのオブジェクトに変化したりと、型の世界の拡張からはみ出している独自機能になっています。TypeScriptプログラマーの中には、この点が受け入れられない人もいます。
数値列挙型には型安全上の問題がある
数値列挙型は、number
型なら何でも代入できるという型安全上の問題点があります。次の例は、値が0
と1
のメンバーだけからなる列挙型ですが、実際にはそれ以外の数値を代入できてしまいます。
この問題はTypeScript5.0未満で発生します。
ts
// TypeScript v4.9.5enumZeroOrOne {Zero = 0,One = 1,}constzeroOrOne :ZeroOrOne = 9; // コンパイルエラーは起きません!
ts
// TypeScript v4.9.5enumZeroOrOne {Zero = 0,One = 1,}constzeroOrOne :ZeroOrOne = 9; // コンパイルエラーは起きません!
TypeScript5.0からは改善されており、コンパイルエラーとなります。
ts
// TypeScript v5.0.4enumZeroOrOne {Zero = 0,One = 1,}constType '9' is not assignable to type 'ZeroOrOne'.2322Type '9' is not assignable to type 'ZeroOrOne'.: zeroOrOne ZeroOrOne = 9;
ts
// TypeScript v5.0.4enumZeroOrOne {Zero = 0,One = 1,}constType '9' is not assignable to type 'ZeroOrOne'.2322Type '9' is not assignable to type 'ZeroOrOne'.: zeroOrOne ZeroOrOne = 9;
列挙型には、列挙型オブジェクトに値でアクセスすると、メンバー名を得られる仕様があります。メンバーに無い値でアクセスしたら、コンパイルエラーになってほしいところですが、そうなりません。
ts
enumZeroOrOne {Zero = 0,One = 1,}console .log (ZeroOrOne [0]); // これは期待どおりconsole .log (ZeroOrOne [9]); // これはコンパイルエラーになってほしいところ…
ts
enumZeroOrOne {Zero = 0,One = 1,}console .log (ZeroOrOne [0]); // これは期待どおりconsole .log (ZeroOrOne [9]); // これはコンパイルエラーになってほしいところ…
文字列列挙型だけ公称型になる
TypeScriptの型システムは、構造的部分型を採用しています。ところが、文字列列挙型は例外的に公称型になります。
ts
enumStringEnum {Foo = "foo",}constfoo1 :StringEnum =StringEnum .Foo ; // コンパイル通るconstType '"foo"' is not assignable to type 'StringEnum'.2322Type '"foo"' is not assignable to type 'StringEnum'.: foo2 StringEnum = "foo"; // コンパイルエラーになる
ts
enumStringEnum {Foo = "foo",}constfoo1 :StringEnum =StringEnum .Foo ; // コンパイル通るconstType '"foo"' is not assignable to type 'StringEnum'.2322Type '"foo"' is not assignable to type 'StringEnum'.: foo2 StringEnum = "foo"; // コンパイルエラーになる
この仕様は意外さがある部分です。加えて、数値列挙型は公称型にならないので、不揃いなところでもあります。
列挙型の代替案
列挙型の代替案をいくつか提示します。ただし、どの代替案も列挙型の特徴を100%再現するものではありません。次の代替案は目的や用途に合う合わないを判断して使い分けてください。
列挙型の代替案1: ユニオン型
もっともシンプルな代替案はユニオン型を用いる方法です。
ts
typeYesNo = "yes" | "no";functiontoJapanese (yesno :YesNo ) {switch (yesno ) {case "yes":return "はい";case "no":return "いいえ";}}
ts
typeYesNo = "yes" | "no";functiontoJapanese (yesno :YesNo ) {switch (yesno ) {case "yes":return "はい";case "no":return "いいえ";}}
ユニオン型とシンボルを組み合わせる方法もあります。
ts
constyes =Symbol ();constno =Symbol ();typeYesNo = typeofyes | typeofno ;functiontoJapanese (yesno :YesNo ) {switch (yesno ) {caseyes :return "はい";caseno :return "いいえ";}}
ts
constyes =Symbol ();constno =Symbol ();typeYesNo = typeofyes | typeofno ;functiontoJapanese (yesno :YesNo ) {switch (yesno ) {caseyes :return "はい";caseno :return "いいえ";}}
列挙型の代替案2: オブジェクトリテラル
オブジェクトリテラルを使う方法もあります。
ts
constPosition = {Top : 0,Right : 1,Bottom : 2,Left : 3,} asconst ;typePosition = (typeofPosition )[keyof typeofPosition ];// 上は type Position = 0 | 1 | 2 | 3 と同じ意味になりますfunctiontoJapanese (position :Position ) {switch (position ) {casePosition .Top :return "上";casePosition .Right :return "右";casePosition .Bottom :return "下";casePosition .Left :return "左";}}
ts
constPosition = {Top : 0,Right : 1,Bottom : 2,Left : 3,} asconst ;typePosition = (typeofPosition )[keyof typeofPosition ];// 上は type Position = 0 | 1 | 2 | 3 と同じ意味になりますfunctiontoJapanese (position :Position ) {switch (position ) {casePosition .Top :return "上";casePosition .Right :return "右";casePosition .Bottom :return "下";casePosition .Left :return "左";}}
まとめ
列挙型の問題点と代替案についても説明しました。特に列挙型は型安全上の問題もあるため、列挙型を積極的に使うかどうかは、よく検討してください。