さようなら、TypeScript enum

2020/02/28
このエントリーをはてなブックマークに追加
ありがとう…看護師…

フロントエンドエンジニアの今村です。TypeScriptではenumを使わずunion型を使いましょう、という話を書きます。

モチベーション

何を今さら、と思う方もいるかもしれません。

TypeScriptのunion型はenum的なものを表現可能であり、基本的にenumよりもunion型を使うべき、という意識を持っているTypeScriptプログラマーはすでに少なからずいるのではないかと思います。しかし、ではenumの使用はいかなる場合も避けるべきなのか、そうでないとしたらどのような基準でenumとunion型を使い分けるべきなのか、といった点について、広く合意の取れたガイドラインはなさそうです(少なくとも私は知りません)。この結果、コードレビューなどで少しやりづらさを感じることがあったので、白黒つけてしまいたいという気持ちからこのブログを書いています。

結論としては、enumは全面的に禁止し、常にunion型を使うのが分かりやすいと考えます。TypeScriptの世界でこの考えに一定の合意が得られ、プログラマーが当たり前のようにリンターでenumの使用を禁止し、enumかunion型かで悩んだり、コードレビューで議論したりすることがなくなればいいと思います。

Stack Overflowでも、enumとunion型の使い分けについての質問があったため、すでに同様の内容を回答しました。もし同意いただけたら、Stack Overflowの方もupvoteしてもらえると嬉しいです。enum廃絶の輪を世界に広げたい。

基礎知識

enum、const enum、union型について簡単に説明します。

コンパイル前:

// 数値のenum
enum NumberEnum {
  // 数値は明示的に割り当てることも可能
  Foo, Bar, Baz
}
const numberEnum: NumberEnum = NumberEnum.Foo;

// 文字列のenum
enum StringEnum {
  Foo = 'foo',
  Bar = 'bar',
  Baz = 'baz'
}
const stringEnum = StringEnum.Foo;

// const enum
// (文字列も利用可能だが、コンパイルに関して挙動の違いはないため数値の例のみ)
const enum ConstEnum {
  Foo,
  Bar,
  Baz
}
const constEnum = ConstEnum.Foo;

// union型
// (文字列も利用可能だが、コンパイルに関して挙動の違いはないため数値の例のみ)
type Union = 0 | 1 | 2;
const union: Union = 0;

コンパイル後:

// 数値のenum
// enumのメンバーの名前と値を双方向にマップするようなJavaScriptオブジェクトが生成される
var NumberEnum;
(function (NumberEnum) {
    NumberEnum[NumberEnum["Foo"] = 0] = "Foo";
    NumberEnum[NumberEnum["Bar"] = 1] = "Bar";
    NumberEnum[NumberEnum["Baz"] = 2] = "Baz";
})(NumberEnum || (NumberEnum = {}));
const numberEnum = NumberEnum.Foo;

// 文字列のenum
// enumのメンバーの名前をキーとし、値を値とするJavaScriptオブジェクトが生成される
var StringEnum;
(function (StringEnum) {
    StringEnum["Foo"] = "foo";
    StringEnum["Bar"] = "bar";
    StringEnum["Baz"] = "baz";
})(StringEnum || (StringEnum = {}));
const stringEnum = StringEnum.Foo;

// const enum
// const enumの宣言自体はJavaScriptのコードに何も出力しない
// const enumの利用箇所で値がインライン化される
const constEnum = 0 /* Foo */;

// union型
// union型の宣言自体はJavaScriptのコードに何も出力しない
const union = 0;

enumはunion型に置き換え可能か

enumを使わないとして、実際にすべてのenumはunion型に置き換え可能なのでしょうか?

反復処理可能なunion型

先述のStack Overflowの質問に対して、accepted answerを含むいくつかの回答で指摘されているのが、union型ではそれに含まれるすべての値に対して反復処理を記述することができない、というものです。

これらの回答がなされた時点では、それはある程度正しかったのですが、TypeScript 3.4で登場したconstアサーション(as constにより、状況は変わりました。現在では、反復処理可能なunion型を簡単に宣言できます。

const permissions = ['read', 'write', 'execute'] as const;
type Permission = typeof permissions[number]; // 'read' | 'write' | 'execute'

// 反復処理
for (const permission of permissions) {
  // 何かやる
}

先に配列を定義してしまって、そこに含まれる要素からunion型を宣言するという方法です。

union型のそれぞれの値に名前をつける

as constはさらに、union型の値そのものだけでは意味が不明瞭な場合に、分かりやすい名前をつけるようなことも可能にしました(ただし、少し手のこんだことをすればas constを使わなくても同様のことが実現できるようです)。

// enumを使う場合
enum Permission {
  Read = 'r',
  Write = 'w',
  Execute = 'x'
}

// union型を使う場合
const Permission = {
  Read: 'r',
  Write: 'w',
  Execute: 'x'
} as const;
type Permission = typeof Permission[keyof typeof Permission]; // 'r' | 'w' | 'x'

// もちろん反復処理可能
for (const permission of Object.values(Permission)) {
  // 何かやる
}

enumの「不透明性」はunion型では実現できない

一方、union型では実現できないenumならではの特徴も残念ながら存在します。文字列のenumを使うとき、ある文字列や文字列リテラルがenumに含まれることが文脈的に明らかであったとしても、それらをenumに割り当てることはできません。

enum StringEnum {
  Foo = 'foo'
}
const foo1: StringEnum = StringEnum.Foo; // no error
const foo2: StringEnum = 'foo'; // error!!

つまり、enum型への値の割り当てでは(アサーションなどを使わない限り)そのenumの値を使うことが強制され、スタイルを統一できます。このような割り当て可能性の挙動は、TypeScriptが構造的型付けを採用していることを考えるとやや奇妙であり、修正すべきであるとするissueも過去には上げられました(これこれ)。これらのissueでこの挙動に対する説明として繰り返し述べられたのは、文字列のenumは「不透明性」を実現するためのものである、ということです。つまり、何かの事情でenumの値の修正が必要になった場合に、この挙動のおかげでその影響範囲を小さくできます。

enum Weekend {
  Saturday = 'Saturday',
  Sunday = 'Sunday'
}
// 直接'Saturday'のような値が割り当てられることはないため、
// 将来Weekend.Saturdayの値が'Sat'になっても変更不要
const weekend: Weekend = Weekend.Saturday;

ただし、この「不透明性」は完全ではなく、片手落ちのやや中途半端なものになってしまっていることには注意が必要です。つまり、上記と逆方向の割り当ては制限されていないのです。

enum Weekend {
  Saturday = 'Saturday',
  Sunday = 'Sunday'
}
// 将来Weekend.Saturdayの値が'Sat'になると変更が必要
const saturday: 'Saturday' = Weekend.Saturday;

この「不透明性」のメリットが、enumのデメリットを上回ると考えるなら、enumは捨てられないことになります。それを判断するために、enumのデメリットを見ていきましょう。

enumのデメリット

1. constでないenumはTypeScriptの”a typed superset of JavaScript”というコンセプトにそぐわない

このコンセプトは、TypeScriptが他のaltJSを差し置いて大きな支持を得た決定的な要因の1つと言えるでしょう。constでないenumは、JavaScriptと互換性のない構文で実行時に存在するJavaScriptオブジェクトを生成することで、このコンセプトに違反します。

2. const enumには落とし穴がある

2-1. const enumはBabelでトランスパイルできない

この問題については、今のところ2つのworkaroundがあります。1つは手でconst enumを通常のenumに書き換えるというもの、もう1つはそれをbabel-plugin-const-enumというプラグインを使ってトランスパイルの過程で自動的に行うというものです。

2-2. アンビエントコンテキストにおけるconst enumの使用は問題になりうる

--isolatedModulesコンパイルオプションを有効にする場合、アンビエントコンテキスト(*.d.tsファイルの中やdeclare構文)で宣言されたconst enumに別モジュール(別ファイル)からアクセスするとコンパイルエラーになります。そのconst enumを内部的にしか利用せず、そのコードがコンパイルされる際のオプションを100%制御できるならいいですが、そうでないなら問題になります。

したがって、enumを実際に使わないようにするかどうかは結局のところ好みの問題もあるでしょうが、npmに公開する*.d.tsでconst enumをexportしたりすることに関しては明確に誤りと言えます。TypeScriptチームのメンバーもこのことを踏まえ、“const enum on DT really does not make sense”(DTはDefinitelyTyped)であり、アンビエントコンテキストにおいては“You should use a union type of literals (string or number) instead.”であると述べています。

2-3. --isolatedModulesが有効な場合のconst enumの挙動はアンビエントコンテキストの外でもおかしい

GitHubのコメントを読んで知ったのですが、--isolatedModulesを有効にしてconst enumをコンパイルすると次のようになります。

コンパイル前:

/// a.ts
export const enum A {
  B
}
export const enum A {
  C = 2
}

/// b.ts
import { A } from './a'

console.log(A.B, A.C)

コンパイル後:

/// a.js
export var A;
(function (A) {
    A[A["B"] = 0] = "B";
})(A || (A = {}));
(function (A) {
    A[A["C"] = 2] = "C";
})(A || (A = {}));
//# sourceMappingURL=a.js.map

/// b.js
console.log(A.B, A.C);
//# sourceMappingURL=b.js.map

--isolatedModulesは、コンパイルを1モジュール(1ファイル)ずつ行えるようにするためのオプションなので、const enumの出力結果が通常のenumのようになるのは想定の範囲内です。別モジュール(別ファイル)に存在するconst enumの定義元の情報が参照できない状況下では、const enumの本来の挙動であるインライン化は実現できません。

注目すべきは、b.jsvar A = require('./a').A;のようなコードが含まれないことです。これはさすがにバグっぽいので、そのうち治るのかもしれませんが、少なくともTypeScript 3.8.2現在修正されていません。

3. 数値のenumは型安全ではない

数値のenumにはあらゆるnumberを割り当て可能です。

enum ZeroOrOne {
  Zero = 0,
  One = 1
}
const zeroOrOne: ZeroOrOne = 2; // no error!!

これについてもTypeScriptチームのメンバーの言葉を借りれば、これはビットフラグの値をenumに使うようなユースケースに対応するための“unfortunate behavior”であり、型安全性が必要な場合はunion型を使うべきであると述べられています。

4. 文字列のenumの宣言は冗長になることがある

こんなenumを書いてうんざりした経験はないでしょうか。

enum Day {
  Sunday = 'Sunday',
  Monday = 'Monday',
  Tuesday = 'Tuesday',
  Wednesday = 'Wednesday',
  Thursday = 'Thursday',
  Friday = 'Friday',
  Saturday = 'Saturday'
}

番外. union型の方がカッコイイ

これはいくらか感情的な話なので、無視していただいても構いません。

先に示したas constを使ってunion型でenumを置き換える書き方は、TypeScriptの柔軟性と表現力の豊さを示す優れた実例です。ネイティブな列挙型のサポートがある言語出身のプログラマーがTypeScriptを使うことになったら、enumを使うことは極めて自然な選択に感じられるかもしれません。しかし、as constを使ったunion型の書き方は、そんなプログラマーに「これがTypeScriptの型システムだ!」と言ってドヤ顔をするのに格好のサンプルです。

このようなパターンを積極的に採用することで、TypeScriptへの理解をより深められ、結果としてよりよいTypeScriptプログラマーになれるでしょう。

enumをコードから排除する方法

typescript-eslintリポジトリのissueのコメントにあるとおり、no-restricted-syntaxというESLintのルールを使うとよいでしょう。

{
  "rules": {
    "no-restricted-syntax": [
      "error",
      {
        "selector": "TSEnumDeclaration",
        "message": "Don't declare enums"
      }
    ]
  }
}

え?TSLintではどうするのかって?まだTSLint使ってるんですか??

まとめ

enumはunion型では実現できない特徴(「不透明性」)を持つものの、その使用に伴うデメリットが大きく、使用すべきでないと考えます。使用を一律禁止することで、個別のケースでenumを使うかunion型を使うかで悩んだり、議論したりする必要もなくなります。ESLintでenumの使用を禁止するには、no-restricted-syntaxルールを用います。

おまけ(宣伝)

3月16日にオライリー・ジャパンから出版される、『プログラミングTypeScript』の監訳を務めさせていただきました。

プログラミングTypeScript

本書では、TypeScriptについて基本的な知識を一通り学んだ上で、その高度な型システムをどのように現実のコードに適用できるかを、多くの実践的なサンプルから身に付けられます。また、JavaScriptからTypeScriptへの移行など、実際のプロジェクトでTypeScriptをどのように導入し、運用するかについての知見も得られます。TypeScriptをこれから学ぶ方におすすめなのはもちろん、すでにTypeScriptの経験がある方が、より自信を持ってTypeScriptを書けるようになるのにも役立つはずです。特に6章の「高度な型」は、熟練のTypeScriptプログラマーにも読み応えのある内容になっていると思います。

また、原著ではTypeScriptのリンターとしてすでに非推奨になったTSLintが紹介されているのですが、日本語版付録としてESLintでTypeScriptをリントする方法の説明を追加しました。この付録には、ASTを操作してESLintのTypeScript向けカスタムルールを実装する方法も記述しています。

ちなみにこの本でも、「enumの安全な使用には落とし穴が伴うため、使用は控えることをお勧めします」という内容が記述されています。

どうぞよろしくお願いいたします。

その他の記事

Other Articles

2020/08/26
TypeScriptで関数の部分型を理解しよう

2020/06/16
[Web フロントエンド] esbuild が爆速すぎて webpack / Rollup にはもう戻れない

2020/03/19
[Web フロントエンド] Elm に心折れ Mint に癒しを求める

2020/02/14
受付のLooking Glassに加えたひと工夫

2020/01/28
カブクエンジニア開発合宿に行ってきました 2020冬

2020/01/30
Renovateで依存ライブラリをリノベーションしよう 〜 Bitbucket編 〜

2019/12/27
Cloud Tasks でも deferred ライブラリが使いたい

2019/12/25
*, ::before, ::after { flex: none; }

2019/12/21
Top-level awaitとDual Package Hazard

2019/12/20
Three.jsからWebGLまで行きて帰りし物語

2019/12/18
Three.jsに入門+手を検出してAR.jsと組み合わせてみた

2019/12/04
WebXR AR Paint その2

2019/11/06
GraphQLの入門書を翻訳しました

2019/09/20
Kabuku Connect 即時見積機能のバックエンド開発

2019/08/14
Maker Faire Tokyo 2019でARゲームを出展しました

2019/07/25
夏休みだョ!WebAssembly Proposal全員集合!!

2019/07/08
鵜呑みにしないで! —— 書籍『クリーンアーキテクチャ』所感 ≪null 篇≫

2019/07/03
W3C Workshop on Web Games参加レポート

2019/06/28
TypeScriptでObject.assign()に正しい型をつける

2019/06/25
カブクエンジニア開発合宿に行ってきました 2019夏

2019/06/21
Hola! KubeCon Europe 2019の参加レポート

2019/06/19
Clean Resume きれいな環境できれいな履歴書を作成する

2019/05/20
[Web フロントエンド] 状態更新ロジックをフレームワークから独立させる

2019/04/16
C++のenable_shared_from_thisを使う

2019/04/12
OpenAPI 3 ファーストな Web アプリケーション開発(Python で API 編)

2019/04/08
WebGLでレイマーチングを使ったCSGを実現する

2019/04/02
『エンジニア採用最前線』に感化されて2週間でエンジニア主導の求人票更新フローを構築した話

2019/03/29
その1 Jetson TX2でk3s(枯山水)を動かしてみた

2019/03/27
任意のブラウザ上でJestで書いたテストを実行する

2019/02/08
TypeScript で “radian” と “degree” を間違えないようにする

2019/02/05
Python3でGoogle Cloud ML Engineをローカルで動作する方法

2019/01/18
SIGGRAPH Asia 2018 参加レポート

2019/01/08
お正月だョ!ECMAScript Proposal全員集合!!

2019/01/08
カブクエンジニア開発合宿に行ってきました 2018秋

2018/12/25
OpenAPI 3 ファーストな Web アプリケーション開発(環境編)

2018/12/23
いまMLKitカスタムモデル(TF Lite)は使えるのか

2018/12/21
[IoT] Docker on JetsonでMQTTを使ってCloud IoT Coreと通信する

2018/12/11
TypeScriptで実現する型安全な多言語対応(Angularを例に)

2018/12/05
GASでCompute Engineの時間に応じた自動停止/起動ツールを作成する 〜GASで簡単に好きなGoogle APIを叩く方法〜

2018/12/02
single quotes な Black を vendoring して packaging

2018/11/14
3次元データに2次元データの深層学習の技術(Inception V3, ResNet)を適用

2018/11/04
Node Knockout 2018 に参戦しました

2018/10/24
SIGGRAPH 2018参加レポート-後編(VR/AR)

2018/10/11
Angular 4アプリケーションをAngular 6に移行する

2018/10/05
SIGGRAPH 2018参加レポート-特別編(VR@50)

2018/10/03
Three.jsでVRしたい

2018/10/02
SIGGRAPH 2018参加レポート-前編

2018/09/27
ズーム可能なSVGを実装する方法の解説

2018/09/25
Kerasを用いた複数入力モデル精度向上のためのTips

2018/09/21
競技プログラミングの勉強会を開催している話

2018/09/19
Ladder Netwoksによる半教師あり学習

2018/08/10
「Maker Faire Tokyo 2018」に出展しました

2018/08/02
Kerasを用いた複数時系列データを1つの深層学習モデルで学習させる方法

2018/07/26
Apollo GraphQLでWebサービスを開発してわかったこと

2018/07/19
【深層学習】時系列データに対する1次元畳み込み層の出力を可視化

2018/07/11
きたない requirements.txt から Pipenv への移行

2018/06/26
CSS Houdiniを味見する

2018/06/25
不確実性を考慮した時系列データ予測

2018/06/20
Google Colaboratory を自分のマシンで走らせる

2018/06/18
Go言語でWebAssembly

2018/06/15
カブクエンジニア開発合宿に行ってきました 2018春

2018/06/08
2018 年の tree shaking

2018/06/07
隠れマルコフモデル 入門

2018/05/30
DASKによる探索的データ分析(EDA)

2018/05/10
TensorFlowをソースからビルドする方法とその効果

2018/04/23
EGLとOpenGLを使用するコードのビルド方法〜libGLからlibOpenGLへ

2018/04/23
技術書典4にサークル参加してきました

2018/04/13
Python で Cura をバッチ実行するためには

2018/04/04
ARCoreで3Dプリント風エフェクトを実現する〜呪文による積層造形映像制作の舞台裏〜

2018/04/02
深層学習を用いた時系列データにおける異常検知

2018/04/01
音声ユーザーインターフェースを用いた新方式積層造形装置の提案

2018/03/31
Container builderでコンテナイメージをBuildしてSlackで結果を受け取る開発スタイルが捗る

2018/03/23
ngUpgrade を使って AngularJS から Angular に移行

2018/03/14
Three.jsのパフォーマンスTips

2018/02/14
C++17の新機能を試す〜その1「3次元版hypot」

2018/01/17
時系列データにおける異常検知

2018/01/11
異常検知の基礎

2018/01/09
three.ar.jsを使ったスマホAR入門

2017/12/17
Python OpenAPIライブラリ bravado-core の発展的な使い方

2017/12/15
WebAssembly(wat)を手書きする

2017/12/14
AngularJS を Angular に移行: ng-annotate 相当の機能を TypeScrpt ファイルに適用

2017/12/08
Android Thingsで4足ロボットを作る ~ Android ThingsとPCA9685でサーボ制御)

2017/12/06
Raspberry PIとDialogflow & Google Cloud Platformを利用した、3Dプリンターボット(仮)の開発 (概要編)

2017/11/20
カブクエンジニア開発合宿に行ってきました 2017秋

2017/10/19
Android Thingsを使って3Dプリント戦車を作ろう ① ハードウェア準備編

2017/10/13
第2回 魁!! GPUクラスタ on GKE ~PodからGPUを使う編~

2017/10/05
第1回 魁!! GPUクラスタ on GKE ~GPUクラスタ構築編~

2017/09/13
「Maker Faire Tokyo 2017」に出展しました。

2017/09/11
PyConJP2017に参加しました

2017/09/08
bravado-coreによるOpenAPIを利用したPythonアプリケーション開発

2017/08/23
OpenAPIのご紹介

2017/08/18
EuroPython2017で2名登壇しました。

2017/07/26
3DプリンターでLチカ

2017/07/03
Three.js r86で何が変わったのか

2017/06/21
3次元データへの深層学習の適用

2017/06/01
カブクエンジニア開発合宿に行ってきました 2017春

2017/05/08
Three.js r85で何が変わったのか

2017/04/10
GCPのGPUインスタンスでレンダリングを高速化

2017/02/07
Three.js r84で何が変わったのか

2017/01/27
Google App EngineのFlexible EnvironmentにTmpfsを導入する

2016/12/21
Three.js r83で何が変わったのか

2016/12/02
Three.jsでのクリッピング平面の利用

2016/11/08
Three.js r82で何が変わったのか

2016/12/17
SIGGRAPH 2016 レポート

2016/11/02
カブクエンジニア開発合宿に行ってきました 2016秋

2016/10/28
PyConJP2016 行きました

2016/10/17
EuroPython2016で登壇しました

2016/10/13
Angular 2.0.0ファイナルへのアップグレード

2016/10/04
Three.js r81で何が変わったのか

2016/09/14
カブクのエンジニアインターンシッププログラムについての詩

2016/09/05
カブクのエンジニアインターンとして3ヶ月でやった事 〜高橋知成の場合〜

2016/08/30
Three.js r80で何が変わったのか

2016/07/15
Three.js r79で何が変わったのか

2016/06/02
Vulkanを試してみた

2016/05/20
MakerGoの作り方

2016/05/08
TensorFlow on DockerでGPUを使えるようにする方法

2016/04/27
Blenderの3DデータをMinecraftに送りこむ

2016/04/20
Tensorflowを使ったDeep LearningにおけるGPU性能調査

→
←

関連職種

Recruit

バックエンドエンジニア(Python・Go)

業務内容

当ポジションは弊社Webサービスのバックエンド機能設計及び実装を担当します。 サービス毎の開発チームで2週間スプリントのスクラム開発を実施しています。 週次で開発チームミーティングを実施し、実装設計の相談や工数見積もりを行います。 全ての開発コードはレビューと自動テストによって品質を保っています。 また、リファクタリングやフレームワークのバージョンアップも開発フローに組込み、技術的負債を放置しない開発を目指しています。

フロントエンドエンジニア(TypeScript)

業務内容

当ポジションは弊社Webサービスのフロントエンド機能設計及び実装を担当します。 サービス毎の開発チームで2週間スプリントのスクラム開発を実施しています。 週次で開発チームミーティングを実施し、実装設計の相談や工数見積もりを行います。 全ての開発コードはレビューと自動テストによって品質を保っています。 また、リファクタリングやフレームワークのバージョンアップも開発フローに組込み、技術的負債を放置しない開発を目指しています。

インターン(Webエンジニア)

業務内容

業務から独立した、調査・研究系のタスクをおまかせしています。コードレビュー、 社内での報告会、 ブログ記事執筆を通して着実にスキルアップしていただくことを目指しています。 (希望があれば、プロダクトの開発業務もおまかせします。)

→
←

お客様のご要望に「Kabuku」はお応えいたします。
ぜひお気軽にご相談ください。

お電話でも受け付けております
03-6380-2750
営業時間:09:30~18:00
※土日祝は除く